Skip to main content
友田 陽大
Vibe coding: shipping AI-generated code safely
バイブコーディング
AI駆動開発
セキュリティ
Supabase
RLS

The security and dangers of vibe coding — what happens when you ship AI-generated code as-is

You built a SaaS with AI (Claude Code / Cursor) last night — is it okay to ship it as-is this morning? This guide walks through the five dangers that show up most often in vibe coding — missing authorization, Supabase RLS design mistakes, injection, hardcoded secrets, and the absence of a verification gate — with real vulnerable→fixed code and 'test it yourself' steps, then leads you to a pre-launch self-audit checklist.

Published
Reading time
13 min read
Author
友田 陽大
Share

Last night I built a SaaS with Claude Code in a few hours. It even has authentication. The demo runs perfectly. — So, can I ship this this morning?

My answer is clear: "Not until you've passed five inspections with your own hands."

This isn't a scare tactic. Solo × generative AI (Claude Code), I have built a B2B SaaS that won the Minister of Economy, Trade and Industry (METI) Award, a payments platform with zero production double-charges, and an AI platform for a broadcaster. That's exactly why I can say it — AI is fast. But left unattended, it mass-produces vulnerabilities at the same speed. And the tricky part is that those vulnerabilities are entirely invisible in the demo. They become visible the moment you launch and someone sends "a well-formed request that points at someone else's ID."

This article shows the five categories of danger that show up especially often in vibe coding, with vulnerable code, fixed code, and "test it yourself" steps. When you finish reading, you can inspect last night's code directly with the pre-launch self-audit checklist at the end of the article.


1. "The demo runs" and "it doesn't leak in production" are different things — one line to ask last night's you

First, let's capture in one line why AI creates dangerous holes. AI writes the "thing you want to do" from the prompt — the happy path — astonishingly fast. But what production asks is how it behaves against malformed input, concurrent access, and a well-formed request that points at someone else's ID — and that's structurally missing.

This is not an opinion. It's a published fact. Veracode's 2025 study found that 45% of AI-generated code contained a known security flaw, and the security scores stayed flat even as the models got smarter (Veracode 2025 GenAI Code Security Report). "Use a smarter model and you'll be safe" unfortunately does not hold.

A deeper look at why AI creates danger (reproducing flaws from training data, lack of context, hallucination) and the full picture of where things break in production are covered in detail in Vulnerability assessment of AI-generated code and Productionizing (hardening) AI-generated code, respectively. To avoid overlap, here I focus on "what the person who built it last night confirms with their own hands before pressing the launch button."

What you should inspect are the following five frequently occurring categories.

#CategoryWhat AI tends to doIn a word
Missing authorization (IDOR)Checks the login but not "is it the owner"Authenticates, but doesn't authorize
RLS design mistakeForgets to put a policy on a Supabase table / puts a loose oneThe DB is the last line of defense, yet the gate is open
InjectionString concatenation / raw SQL / dangerouslySetInnerHTMLTreats external input as "assumed correct"
Hardcoded secretsWrites API keys or service_role directly into the codeTaping the key to the door and leaving
Absence of a verification gateFix it once, and the next generation revives the same holeWhat you fixed gets undone by the next generation

Below, we crush them one by one with "vulnerable → fixed → test it yourself."


2. Category ①: missing authorization (IDOR) — just shift the URL's ID by one

This is the most frequent and the most serious. AI writes the authentication (is the user logged in), but it omits the authorization (is this person the owner of that data).

// ❌ AIがよく書く:ログインは確認するが「所有者か」を確認しない
export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const session = await getSession();
  if (!session) return new Response("Unauthorized", { status: 401 });

  // params.id は誰のものでもチェックしていない → 他人の注文が見える
  const order = await db.order.findUnique({ where: { id: params.id } });
  return Response.json(order);
}

It only looks at whether session exists and never checks whether params.id belongs to that person. A logged-in attacker just changes /api/orders/123 to /api/orders/124 and someone else's data comes back. This is IDOR (Insecure Direct Object Reference) / BOLA (Broken Object Level Authorization), the most frequent risk that has been #1 ever since 2019 in the OWASP API Security Top 10 (OWASP API1:2023 BOLA).

The fix is to always include ownership in the query.

// ✅ 「持ち主か」をクエリで強制する
const order = await db.order.findFirst({
  where: { id: params.id, userId: session.userId }, // 所有者スコープを付与
});
if (!order) return new Response("Not Found", { status: 404 });
return Response.json(order);

Steps to test it yourself (done in minutes):

  1. Log in with two accounts, A and B
  2. As A, note a resource's ID (e.g., /api/orders/123)
  3. Staying in B's session, replace the ID in the URL or API with A's value and send it
  4. If A's data comes back — that's IDOR. You must not launch

If you want the fastest understanding of this "authenticates but doesn't authorize" hole through real vulnerable→fixed code, read alongside Authenticates but doesn't authorize: the owner-scope pitfall.


3. Category ②: Supabase RLS design mistakes — the DB is the last line of defense, yet it's open

If you use Supabase, the last line of defense for authorization is Row Level Security (RLS). This is where AI-generated apps are most often missing.

There are two stages to the problem. (a) You forget to put a policy at all, and (b) you put one but it's loose.

-- ❌ (a) RLSを有効化していない → anonキーで全行が読み書きできる
create table public.notes (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references auth.users(id),
  body text
);
-- ここで ENABLE ROW LEVEL SECURITY を忘れると、テーブルは全公開

-- ❌ (b) 有効化はしたが「認証はするが所有者に絞らない」
create policy "read notes" on public.notes
  for select using (auth.role() = 'authenticated'); -- ログインさえしていれば全員の行が見える

(b) is especially nasty. Anyone logged in can read everyone else's notes too. This is the DB version of IDOR, a textbook case of "authenticates but doesn't authorize." The fix is to scope rows to the owner.

-- ✅ 所有者スコープを USING に入れる。書き込みは WITH CHECK も必須
alter table public.notes enable row level security;

create policy "read own notes" on public.notes
  for select using (auth.uid() = owner_id);

create policy "insert own notes" on public.notes
  for insert with check (auth.uid() = owner_id); -- WITH CHECK が無いと他人名義で挿入できる

This isn't my armchair theory — it's a frequent pattern confirmed in public data. In a primary survey of 1,000 public Supabase apps, roughly 9.2% of the apps that had RLS carried a policy that "authenticates but doesn't scope rows to the owner" (a lower bound, since the sample is public repositories; non-owner-scoped ≠ immediately vulnerable — it needs checking). The methodology and all the numbers from this survey are collected in the Supabase RLS field study.

As a caveat, USING (true) is not always evil. If the table is intended to be "public data anyone may read," that's the correct answer. What's dangerous is unconsciously placing true on a table of personal data that should really be scoped to the owner. So it's not "non-owner-scoped = immediately vulnerable" but "confirm the intent" — you have to judge whether it's meant to be shared or is a forgotten scope.

A case where, in an AI-builder-made app without RLS, an unauthenticated attacker could actually read and write other people's data is registered as a CVE (CVE-2025-48757, CWE-863, CVSS 9.3 CRITICAL). This is what happens to an app whose "demo was running."


4. Category ③: injection — treating external input as "assumed correct"

AI tends to treat input coming from forms, URLs, and APIs as "assumed correct." In production, unless you validate and sanitize all external input at the boundary, injection breaks you.

There are two representatives: SQL injection and XSS.

// ❌ SQLインジェクション:ユーザー入力を文字列連結で埋め込む
const rows = await db.$queryRawUnsafe(
  `SELECT * FROM users WHERE email = '${email}'` // ' OR '1'='1 で全件返る
);

// ❌ XSS:ユーザー入力をそのままHTMLに挿す
<div dangerouslySetInnerHTML={{ __html: comment }} /> // <script> が実行される

The principles for the fix are "parameterize, don't concatenate," "don't inject HTML — escape it," and typed validation at the boundary.

// ✅ パラメータ化クエリ(プレースホルダにユーザー値を渡す)
const rows = await db.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

// ✅ 入力は境界で Zod 検証してから使う
const Body = z.object({ email: z.string().email(), qty: z.number().int().positive() });
const { email, qty } = Body.parse(await req.json()); // 想定外の形は即座に弾く

// ✅ HTMLはそのまま挿さない。表示はテキストとして、必要ならサニタイズ後に
<div>{comment}</div> // React はデフォルトでエスケープする

The point is not to settle for "I think I wrote validation." z.string() alone leaves both length and format unrestricted. Place meaningful constraints at the boundary — an email for an email, a positive integer for a quantity.


5. Category ④: hardcoded secrets — taping the key to the door and leaving

AI prioritizes making things run and tends to write API keys and tokens directly into the code. And the most dangerous mix-up in vibe coding is exposing Supabase's service_role key to the client.

// ❌ 最悪:service_role キーをクライアント/NEXT_PUBLIC に置く
const supabase = createClient(
  "https://xxxx.supabase.co",
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // ブラウザに漏れる = 全データ露出
);

Why is it fatal? The service_role key runs with PostgreSQL's BYPASSRLS privilege and ignores RLS entirely (Supabase Docs — Row Level Security). That is, no matter how carefully you set up RLS, the moment this key reaches the browser, every table becomes readable and writable regardless of whether RLS exists. The defense boundary shifts from the policy to "where you keep the key."

The principle is simple.

KeyWhere to use itNature
anon (public key)Client OKFollows RLS. Safe on the premise of RLS
service_roleServer onlyIgnores RLS. Never expose it to the client
  • Use service_role only inside the server (Route Handler / Server Action).
  • Environment variables prefixed with NEXT_PUBLIC_ are baked into the build and visible from the browser — never attach a secret to one.
  • Put API keys and tokens in env, not in code (and don't commit env).

Also, when you create a SECURITY DEFINER function, pin the search_path (e.g., SET search_path = ''). A function that runs with definer privileges can become a bypass around RLS if its search_path is poisoned. It's also worth reconsidering whether that function really needs to be SECURITY DEFINER in the first place.

Steps to test it yourself: open the post-build .next, or the browser devtools Network / Sources, and search for strings containing service_role or any familiar secret to check whether they've leaked to the client side. A single grep prevents many incidents.


6. Category ⑤: the absence of a verification gate — what you fixed gets undone by the next generation

Suppose you fixed all four so far. It's still not enough. Why? — because the moment AI generates code next, the same hole comes back.

This is a pitfall specific to vibe coding. Even if you close it once with a manual review, if tomorrow you throw the prompt "add this feature too," the AI writes another route with authorization omitted. A one-time visual check loses to the speed of AI-driven development.

So you build verification into CI, so it's mechanically re-checked on every release. Generation speed stays the same; you just insert one mechanical gate before shipping.

# 例:PRごとに静的解析を回し、高信頼の検出だけをビルド失敗にする
name: security-gate
on: pull_request
jobs:
  scan:
    runs-on: ubuntu-latest
    permissions:
      security-events: write   # SARIF を GitHub code scanning に上げる
    steps:
      - uses: actions/checkout@v4
      - run: npx @aegiskit/cli ci   # 高信頼の検出のみビルドをブロック

The thinking behind this gate design itself (pinning types, tests, and security in CI) is collected in Quality gates for AI-driven development, and the approach of letting AI implement while a human holds the spec and verification is in Putting Claude Code into production with spec-driven development. The gist is the same — AI implements; a human owns spec decisions and verification. This division of labor makes speed safe.


7. The "horizontal" you can automate, and the "vertical" you confirm yourself

Looking at the five categories, you'll notice: some things can be left to a machine, and some only you can judge.

  • Horizontal controls — headers/CSP, rate limiting, input validation, CSRF, secret-leakage detection. Because these apply uniformly across apps, a library can take them over.
  • Vertical risks — authorization/IDOR, Supabase RLS design, tenant isolation. Because these depend on your business rules of "who owns what," a machine can't make the final call.

The free OSS Aegis I maintain is designed not to blur this boundary. npx @aegiskit/cli scan (/aegis) automates the horizontal controls, and detects and warns on the vertical risks (authorization/IDOR, RLS design) — and that's as far as it goes.

# インストール不要。いまのプロジェクトを静的解析する
npx @aegiskit/cli scan

To be honest, no tool can claim to "protect you completely." A product that advertises so is, if anything, dangerous (the complacency of "I installed it, so I'm fine" produces the worst outcomes). Aegis complements RLS design, code review, and threat modeling — it does not replace them. A clean result means "you haven't stepped on the common traps," not "this code is safe." That's exactly why you need to confirm the vertical risks last, with your own eyes.


8. Answer "can I launch this morning?" yourself

Back to the opening question. The SaaS you built last night — can you launch it this morning?

The answer comes down to whether you passed the following five points with your own hands.

  1. Authorization (IDOR) — did you swap in another account's ID, hit it, and confirm that someone else's data doesn't come back?
  2. RLS — did you enable RLS on all tables and put owner scope (auth.uid() = owner_id) and WITH CHECK? Is USING(true) only on tables meant to be public?
  3. Injection — did you parameterize raw SQL, remove dangerouslySetInnerHTML, and validate with Zod at the boundary?
  4. Secrets — did you avoid exposing service_role to the client, and avoid attaching a secret to NEXT_PUBLIC_?
  5. Verification gate — did you build this inspection into CI so it's automatically re-checked on the next generation too?

I've collected these five perspectives into a more detailed 25-item checklist, distributed for free. Each item is built so that you don't just "read" it but confirm whether you can actually move your hands and answer "yes." With last night's code still open, run through it from the top.

Get the AI-generated-code security self-audit checklist (Vibe Coding edition) → Five perspectives, 25 items: authorization, Supabase RLS, input validation, secrets, and the pre-release gate. Before you launch, with your own hands.

One last time. Meeting every item does not guarantee safety. This is the minimum bar for crushing known, typical holes. If the data you handle is highly sensitive (payments, personal data, medical, and so on), consider a review or assessment by an expert on top of this. If you need a design review to actually close the vertical risks that were detected, I'm available.

Building fast is right. The problem is launching what you built fast without checking it. The speed of vibe coding turns directly into production quality through this five-minute inspection before launch.

Frequently asked questions

The SaaS I built with AI last night — can I ship it as-is this morning?
Not yet — it's dangerous. 'The demo runs' and 'it doesn't leak in production' are different problems. At a minimum, inspect five points with your own hands before launching: authorization (confirm IDOR by hitting it yourself), Supabase RLS, input validation, secrets, and a pre-release gate. Meeting every item doesn't guarantee safety, but it does close the large majority of the known, typical holes.
Which is the most dangerous hole in AI-generated code?
Missing authorization (IDOR). AI writes the authentication of 'is the user logged in,' but it tends to skip the authorization of 'does this data belong to them.' Swap the ID in the URL or API for someone else's value, make the request yourself, and if someone else's data comes back — that's a fail. You can self-diagnose it in minutes, so always try it before launch.
Will I be safe if I install a free OSS?
No. `npx @aegiskit/cli scan` goes as far as automating the horizontal controls — headers/CSP, rate limiting, input validation, secret leakage — and detecting and warning on the vertical risks of authorization/IDOR and RLS design. Actually closing the vertical risk is your design decision, and no tool can claim to 'protect you completely.' The complacency of 'I installed it, so I'm fine' produces the worst outcomes.
What's the difference between the service_role key and the anon key?
The anon key follows RLS, but the service_role key runs with PostgreSQL's BYPASSRLS and ignores RLS entirely. So use service_role only inside the server, and never expose it to the client, NEXT_PUBLIC, or the browser. The moment it's out, every table becomes readable and writable regardless of whether RLS exists.
Is a one-time self-audit enough?
It's not enough. The next time AI generates code, the same hole comes back. So build the inspection into CI, so it's mechanically re-checked on every release. A mechanism that stops recurrence by machine works far more reliably than a one-time visual check.

References

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

That AI-generated code — safe to deploy as-is?

After “it works” comes “it doesn't leak” — scan your project free with one command

Aegis, an MIT-licensed OSS, automates the controls AI tends to skip (headers/CSP, rate limiting, input validation, secrets) and detects-and-warns on vertical risks in authorization and RLS design. The fix plan from `aegis fix --format json` pastes straight into Claude Code or Cursor.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading