昨夜、Claude Code と数時間でSaaSを作った。認証もついている。デモは完璧に動く。——では、今朝これを公開していいか?
私の答えは明確です。「5つの点検を、自分の手で通すまでは、まだダメ」。
これは脅しではありません。私は一人 × 生成AI(Claude Code)で、経済産業大臣賞を受賞したB2B SaaS、本番二重課金0件の決済基盤、放送局向けのAIプラットフォームを作ってきました。だからこそ言えます——AIは速い。しかし放っておくと、脆弱性も同じ速さで量産します。 そして厄介なことに、その脆弱性はデモでは一切見えません。見えるのは、公開して、誰かが「他人のIDを指す正規リクエスト」を送ってきた瞬間です。
本記事は、バイブコーディングで特に頻出する5類型の危険を、脆弱コードと修正コード、そして**「自分で叩いて確かめる」手順**で示します。読み終えたら、記事末尾の公開前セルフ診断チェックリストで、あなたの昨夜のコードをそのまま点検できます。
1. 「デモは動く」と「本番で漏れない」は別物 — 昨夜の自分に問う一行
まず、なぜAIが危険な穴を生むのかを一行で押さえます。AIは、プロンプトで指示された「やりたいこと」=ハッピーパスを驚くほど速く書く。しかし本番で問われるのは、不正な入力・同時アクセス・他人のIDを指す正規リクエストにどう振る舞うか——ここが構造的に抜けます。
これは感想ではありません。公開された事実です。Veracode の2025年の調査では、AIが生成したコードの45%が既知のセキュリティ欠陥を含み、モデルが賢くなってもセキュリティ成績は横ばいでした(Veracode 2025 GenAI Code Security Report)。「賢いモデルを使えば安全になる」は、残念ながら成り立ちません。
なぜAIが危険を生むかの掘り下げ(訓練データの欠陥再生産・文脈の欠如・幻覚)と、本番で壊れる箇所の全体像は、それぞれAI生成コードの脆弱性診断とAI生成コードの本番化(ハードニング)で詳しく書きました。ここでは重複を避け、**「昨夜作った自分が、公開ボタンを押す前に何を自分の手で確かめるか」**に絞ります。
点検すべきは、頻出する次の5類型です。
| # | 類型 | AIがやりがちなこと | 一言で言うと |
|---|---|---|---|
| ① | 認可の欠落(IDOR) | ログインは確認するが「持ち主か」を確認しない | 認証はするが、認可しない |
| ② | RLSの設計ミス | Supabaseのテーブルにポリシを張り忘れる/緩く張る | DBが最後の砦なのに、砦が開いている |
| ③ | 注入(injection) | 文字列連結・生SQL・dangerouslySetInnerHTML | 外部入力を「正しい前提」で扱う |
| ④ | 秘密情報のハードコード | APIキーや service_role をコードに直書き | 鍵をドアに貼って出かける |
| ⑤ | 検証ゲートの不在 | 一度直しても、次の生成で同じ穴が復活 | 直したことが、次の生成で無かったことになる |
以下、1つずつ「脆弱→修正→自分で確かめる」で潰していきます。
2. 類型①:認可の欠落(IDOR)— URLのIDを1つずらすだけ
最頻出かつ最重大です。 AIは認証(ログイン済みか)は書きますが、認可(その人がそのデータの持ち主か)を省略します。
// ❌ 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);
}
session の有無だけを見て、params.id がその人のものかを一切見ていません。ログイン済みの攻撃者が /api/orders/123 を /api/orders/124 に変えるだけで、他人のデータが返ってきます。これが IDOR(Insecure Direct Object Reference)/ BOLA(Broken Object Level Authorization) で、OWASP API Security Top 10 では2019年以来ずっと第1位の最頻出リスクです(OWASP API1:2023 BOLA)。
修正は、所有権を必ずクエリに含めることです。
// ✅ 「持ち主か」をクエリで強制する
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);
自分で確かめる手順(数分で終わる):
- 2つのアカウントA・Bでログインする
- AでリソースのID(
/api/orders/123など)を控える - Bのセッションのまま、URLかAPIのIDをAの値に差し替えて送る
- Aのデータが返ってきたら——それがIDORです。公開してはいけません
この「認証はするが認可しない」という穴を、脆弱→修正の実コードで最短理解したい場合は、認証はするが認可しない:所有者スコープの落とし穴を併読してください。
3. 類型②:Supabase RLS の設計ミス — DBが最後の砦なのに開いている
Supabase を使うなら、認可の最後の砦は**行レベルセキュリティ(RLS)**です。ここが、AI生成アプリで最も抜けます。
問題は2段階あります。(a) そもそもポリシを張り忘れる、(b) 張ったが緩い。
-- ❌ (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) が特に厄介です。ログインした人なら、他人のノートも全部読める。これはIDORのDB版で、「認証はするが認可しない」の典型です。修正は、所有者に行を絞ることです。
-- ✅ 所有者スコープを 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 が無いと他人名義で挿入できる
これは私の空論ではなく、公開データで確認された頻出パターンです。公開Supabaseアプリ1,000件の一次調査で、RLSを持つアプリのうち約9.2%が『認証はするが行を所有者に絞らない』ポリシを持っていた(公開リポジトリ標本ゆえ下限値。所有者非スコープ=即脆弱ではなく要確認)。この調査の手法と全数字はSupabase RLS 実地調査(field study)にまとめています。
補足として、USING (true) は常に悪ではありません。 そのテーブルが「誰でも読んでよい公開データ」であることを意図しているなら、それが正解です。危険なのは、本来は所有者に絞るべき個人データのテーブルに、無意識に true を置いてしまうことです。だから「所有者非スコープ=即脆弱」ではなく、「意図を確認せよ」——共有前提なのか、絞り忘れなのかを、あなたが判断する必要があります。
RLSを持たないAIビルダー製アプリで、実際に未認証の攻撃者が他人のデータを読み書きできた事例は、CVE として登録されています(CVE-2025-48757、CWE-863、CVSS 9.3 CRITICAL)。「デモは動いていた」アプリが、こうなります。
4. 類型③:注入(injection)— 外部入力を「正しい前提」で扱う
AIは、フォーム・URL・APIから来る入力を**「正しい前提」で扱いがち**です。本番では、すべての外部入力を境界で検証・サニタイズしないと、インジェクションで壊れます。
代表は2つ。SQLインジェクションとXSSです。
// ❌ SQLインジェクション:ユーザー入力を文字列連結で埋め込む
const rows = await db.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${email}'` // ' OR '1'='1 で全件返る
);
// ❌ XSS:ユーザー入力をそのままHTMLに挿す
<div dangerouslySetInnerHTML={{ __html: comment }} /> // <script> が実行される
修正の原則は**「連結ではなくパラメータ化」「HTMLは挿さずエスケープ」、そして境界での型付き検証**です。
// ✅ パラメータ化クエリ(プレースホルダにユーザー値を渡す)
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 はデフォルトでエスケープする
ポイントは、「検証を書いたつもり」で満足しないこと。z.string() だけでは長さも形式も無制限です。メールならメール、数量なら正の整数、というように意味のある制約を境界に置いてください。
5. 類型④:秘密情報のハードコード — 鍵をドアに貼って出かける
AIは、動かすことを優先してAPIキーやトークンをコードに直書きしがちです。そしてバイブコーディングで最も危険な取り違えが、Supabase の service_role キーをクライアントに出すことです。
// ❌ 最悪:service_role キーをクライアント/NEXT_PUBLIC に置く
const supabase = createClient(
"https://xxxx.supabase.co",
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // ブラウザに漏れる = 全データ露出
);
なぜ致命的か。service_role キーは PostgreSQL の BYPASSRLS 権限で動き、RLSを完全に無視します(Supabase Docs — Row Level Security)。つまり、どれだけ丁寧にRLSを張っても、このキーがブラウザに出た瞬間、RLSの有無に関係なく全テーブルが読み書き可能になります。守りの境界は、ポリシから「鍵の置き場所」に移るのです。
原則はシンプルです。
| キー | 使う場所 | 性質 |
|---|---|---|
anon(公開キー) | クライアント可 | RLSに従う。RLS前提で安全 |
service_role | サーバ内のみ | RLSを無視する。絶対にクライアントへ出さない |
service_roleはサーバ(Route Handler / Server Action)内だけで使うNEXT_PUBLIC_を付けた環境変数はビルドに焼き込まれ、ブラウザから見える——秘密を絶対に付けない- APIキー・トークンはコードに書かず env に置く(そして env はコミットしない)
また、SECURITY DEFINER 関数を作る場合は、search_path を固定してください(SET search_path = '' など)。定義者権限で動く関数は、search_path を汚染されるとRLSの迂回路になり得ます。そもそもその関数が本当に SECURITY DEFINER である必要があるかも、併せて見直す価値があります。
自分で確かめる手順: ビルド後の .next や、ブラウザの devtools の Network / Sources を開き、service_role を含む文字列や見覚えのある秘密がクライアント側に出ていないかを検索してください。1回 grep するだけで、多くの事故は防げます。
6. 類型⑤:検証ゲートの不在 — 直したことが、次の生成で無かったことになる
ここまでの4つを全部直したとします。それでも、まだ足りません。 なぜなら——次にAIがコードを生成した瞬間、同じ穴が復活するからです。
これがバイブコーディング特有の落とし穴です。手作業のレビューで1回塞いでも、明日あなたが「この機能も追加して」とプロンプトを投げれば、AIはまた認可を省いたルートを書きます。一度きりの目視は、AI駆動開発の速度に負けます。
だから、検証をCIに組み込んで、リリースのたびに機械的に再チェックされる状態にします。生成の速さはそのまま、出荷の前に機械的なゲートを1枚挟むわけです。
# 例: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 # 高信頼の検出のみビルドをブロック
このゲート設計そのものの考え方(型・テスト・セキュリティをCIで固定する)はAI駆動開発の品質ゲートに、AIに実装させつつ人間が仕様と検証を握る進め方は仕様駆動でClaude Codeを本番運用に載せるにまとめています。要点は同じ——AIに実装、人間に仕様決定と検証。この役割分担が、速さを安全にします。
7. 自動化できる「水平」と、自分で確かめる「縦」
5類型を見て気づくと思います。機械に任せられるものと、あなたにしか判断できないものがある、と。
- 水平の統制——ヘッダー/CSP・レート制限・入力検証・CSRF・秘密情報の混入検知。これはアプリ横断で一律に効くので、ライブラリが肩代わりできます。
- 縦のリスク——認可/IDOR・Supabase RLSの設計・テナント分離。これは**「誰が何を所有するか」というあなたの事業ルール**に依存するので、機械には最終判断ができません。
私がメンテナンスしている無料OSS Aegis は、この境界を誤魔化さない設計です。npx @aegiskit/cli scan(/aegis)は、水平の統制を自動化し、縦のリスク(認可/IDOR・RLS設計)は検出して警告する——ここまでです。
# インストール不要。いまのプロジェクトを静的解析する
npx @aegiskit/cli scan
正直に言えば、どんなツールも「完全に守る」とは言えません。そう謳う製品はむしろ危険です(「入れたから大丈夫」という油断が最悪の結果を生む)。Aegis は RLS の設計・コードレビュー・脅威モデリングを補完するもので、置き換えるものではありません。クリーンな結果が意味するのは「よくある罠は踏んでいない」であって、「このコードは安全だ」ではないのです。だからこそ、縦のリスクは最後、あなた自身の目で確かめる必要があります。
8. 「今朝公開していい?」に、自分で答える
冒頭の問いに戻ります。昨夜作ったSaaS、今朝公開していいか。
答えは、次の5点を自分の手で通せたかどうかで決まります。
- 認可(IDOR) — 別アカウントのIDに差し替えて叩き、他人のデータが返らないことを確認したか
- RLS — 全テーブルでRLSを有効化し、所有者スコープ(
auth.uid() = owner_id)とWITH CHECKを張ったか。USING(true)は公開前提のテーブルだけか - 注入 — 生SQLをパラメータ化し、
dangerouslySetInnerHTMLを排し、境界で Zod 検証したか - 秘密情報 —
service_roleをクライアントに出していないか、NEXT_PUBLIC_に秘密を付けていないか - 検証ゲート — この点検をCIに組み込み、次の生成でも自動再チェックされる状態にしたか
この5観点を、より細かい25項目のチェックリストにまとめ、無料で配布しています。各項目は「読む」のではなく、実際に手を動かして『はい』と答えられるかを確かめる作り方です。昨夜のコードを開いたまま、上から順に通してみてください。
AI生成コード セキュリティ・セルフ診断チェックリスト(Vibe Coding版)を受け取る → 認可・Supabase RLS・入力検証・秘密情報・リリース前ゲートの5観点25項目。公開前に、自分の手で。
最後にもう一度だけ。全項目を満たしても、安全が保証されるわけではありません。 これは既知の典型的な穴を潰すための最低ラインです。扱うデータの重要度が高いなら(決済・個人情報・医療など)、そのうえで専門家によるレビューや診断も検討してください。検出された縦のリスクを実際に塞ぐ設計レビューが必要なら、私が承ります。
速く作ることは、正しい。問題は、速く作ったものを、確かめずに公開することです。 バイブコーディングの速度は、公開前のこの5分の点検で、そのまま本番品質に変わります。