# 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: 2026-07-07
- Author: 友田 陽大
- Tags: バイブコーディング, AI駆動開発, セキュリティ, Supabase, RLS
- URL: https://tomodahinata.com/en/blog/vibe-coding-security-risks-self-check-guide
- Category: Vibe coding: shipping AI-generated code safely
- Pillar guide: https://tomodahinata.com/en/blog/vibe-coding-what-is-tools-risks-production-guide

## Key points

- Before you ship the SaaS you built with AI last night straight out the next morning, inspect five points with your own hands: authorization, RLS, injection, secrets, and the verification gate. 'The demo runs' and 'it doesn't leak in production' are entirely different problems.
- The most frequent and most serious is missing authorization (IDOR). AI writes 'is the user logged in (authentication)' but skips 'does this data belong to them (authorization).' Swap the ID in the URL for someone else's value and hit it yourself, and you can confirm it in minutes before launch.
- In Supabase, authorization is enforced by RLS at the DB. Policies that 'authenticate but don't scope rows to the owner' are common — even in a primary survey of 1,000 public apps, roughly 9.2% of the apps that had RLS matched (a lower bound; needs checking). USING(true) is the right answer only when the table is meant to be public.
- Don't hardcode secrets. In particular, Supabase's service_role key ignores RLS entirely, so the moment it reaches the client or NEXT_PUBLIC, all your data is exposed. The defense boundary shifts to where you keep the key.
- Fix it once and the same hole comes back the next time AI generates. Leave automating the horizontal controls to the free OSS npx @aegiskit/cli scan, and keep verifying the vertical risk of authorization/RLS design with your own eyes and a CI gate.

---

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](/resources) 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](https://www.veracode.com/resources/analyst-reports/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](/blog/ai-generated-code-vulnerability-assessment-vibe-coding-security-guide) and [Productionizing (hardening) AI-generated code](/blog/vibe-coding-ai-generated-code-production-hardening-guide), 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.

| # | Category | What AI tends to do | In a word |
|---|---|---|---|
| ① | Missing authorization (IDOR) | Checks the login but not "is it the owner" | Authenticates, but doesn't authorize |
| ② | RLS design mistake | Forgets to put a policy on a Supabase table / puts a loose one | The DB is the last line of defense, yet the gate is open |
| ③ | Injection | String concatenation / raw SQL / `dangerouslySetInnerHTML` | Treats external input as "assumed correct" |
| ④ | Hardcoded secrets | Writes API keys or `service_role` directly into the code | Taping the key to the door and leaving |
| ⑤ | Absence of a verification gate | Fix it once, and the next generation revives the same hole | What 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).**

```ts
// ❌ 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](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/)).

The fix is to **always include ownership in the query.**

```ts
// ✅ 「持ち主か」をクエリで強制する
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](/blog/supabase-rls-authenticated-vs-authorized-owner-scope-guide).

---

## 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.**

```sql
-- ❌ (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.**

```sql
-- ✅ 所有者スコープを 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](/blog/supabase-rls-security-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](https://nvd.nist.gov/vuln/detail/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.**

```ts
// ❌ 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.**

```ts
// ✅ パラメータ化クエリ（プレースホルダにユーザー値を渡す）
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.**

```ts
// ❌ 最悪：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](https://supabase.com/docs/guides/database/postgres/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.

| Key | Where to use it | Nature |
|---|---|---|
| `anon` (public key) | Client OK | Follows RLS. Safe on the premise of RLS |
| `service_role` | **Server only** | Ignores 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.

```yaml
# 例：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](/blog/ai-driven-development-quality-gates-ci-type-safety-test-security), 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](/blog/spec-driven-development-claude-code-ai-agent-production-workflow). 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](/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.

```bash
# インストール不要。いまのプロジェクトを静的解析する
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) →](/resources)**
> 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.
