# AIが生成したSupabase RLSポリシは信用できるか — 実測1,000アプリの知見と見抜き方

> Claude CodeやCursorが生成するSupabase RLSは『動く』が『認可になっている』とは限りません。USING(true)・authenticated全許可・WITH CHECK欠落・RLS未有効化・SECURITY DEFINERのsearch_path未固定——AIが吐く定型ミスを実SQLでカタログ化し、公開アプリ1,000件で約9.2%という実測の下限値とともに、貼るだけのチェッカーやscanで見抜く検証ワークフローを解説します。

- 公開日: 2026-07-07
- 著者: 友田 陽大
- タグ: Supabase, RLS, セキュリティ, バイブコーディング, AI駆動開発, PostgreSQL, Next.js
- URL: https://tomodahinata.com/blog/supabase-rls-ai-generated-policy-review-guide
- カテゴリ: バイブコーディング
- 総合ガイド: https://tomodahinata.com/blog/vibe-coding-what-is-tools-risks-production-guide

## 要点

- AIが生成するRLSの失敗は無限のバリエーションではなく、少数の定型に収束する——USING(true)の全開、auth.role()='authenticated'による認証のみ許可、WITH CHECK欠落、RLS未有効化、SECURITY DEFINERのsearch_path未固定。だから『信用する/しない』ではなく、機械的に洗い出して1件ずつ確認するのが正解。
- これらが量産される理由は注意不足ではなく構造にある。プロンプトの字義実装（『ログインユーザーが読める』→auth.role()='authenticated'）と、開発者が自分のアカウント1つしか使わないためデモで露見しないこと。AIが賢くなっても構造は変わらない。
- ただし『定型に見える＝脆弱』ではない。USING(true)は共有マスタなら正当、service_roleゲートやselect auth.uid()ラップは正しい形。AIの出力を機械的に叩くと、この紛らわしい正解を誤検知しがち——実データで潰さないと数字は信用できない。
- 公開Supabaseアプリ1,000件の一次調査で、RLSを持つアプリのうち約9.2%が『認証はするが行を所有者に絞らない』ポリシを持っていた（公開リポジトリ標本ゆえ下限値。所有者非スコープ＝即脆弱ではなく要確認）。特殊事例ではなく、AIの標準的な出力傾向。
- 検証は生成ループに1枚のゲートとして埋める：貼るだけのブラウザRLSチェッカー→リポジトリ全体を npx @aegiskit/cli scan→pgTAPで回帰固定。ツールは述語の『形』を機械化するだけで、共有意図の正しさとテナント分離の妥当性は人間の設計判断＝監査に残る。

---

結論から書きます。**AIが生成したSupabase RLSポリシは「動く」——けれど「認可になっている」とは限りません。そして厄介なのは、その失敗が無限のバリエーションではなく、少数の定型に収束することです。** `USING (true)` の全開、`auth.role() = 'authenticated'` の認証のみ許可、`WITH CHECK` の欠落、`ENABLE ROW LEVEL SECURITY` の付け忘れ、`SECURITY DEFINER` の `search_path` 未固定——毎日Claude Codeで出荷している私が、AIにRLSを書かせて実際に何度も見てきた「定型ミス」は、片手で数えられる程度の種類しかありません。だから答えは「AIを信用する／しない」ではなく、**「どうせ同じ場所で滑る」と分かっているものを、機械的に洗い出して1件ずつ確認する**ことです。

私はAIを敵視していません。むしろ生成の速度は最大の武器です。一人で経済産業大臣賞のB2B SaaSや本番二重課金0件の決済基盤を出せたのは、AIに実装を任せて自分は設計と検証に集中したからです。ただし、その速度を本番品質に変えるのは、生成の後ろに置いた**検証ゲート**です。本記事は、(1) なぜAIのRLSは同じ場所で滑るのか、(2) その定型ミスの実SQLカタログ、(3) 逆に「正しいのに紛らわしい」形、(4) これが特殊事例でない実測、(5) 生成ループへの検証の埋め込み方、までを一気に扱います。個々のパターンの深掘りは、既存のRLS正典記事に委ねます——ここで全部を再解説はしません。

なお、AIが生成したコード全般をどう診断し、本番に固めるかという買い手視点の全体像は[AI生成コードの脆弱性評価ガイド](/blog/ai-generated-code-vulnerability-assessment-vibe-coding-security-guide)と[バイブコーディングの本番ハードニングガイド](/blog/vibe-coding-ai-generated-code-production-hardening-guide)にまとめています。本記事は、その中でも最も漏れやすい「RLSポリシの述語」に絞った実装者向けの深掘りです。

---

## 1. なぜAIのRLSは同じ場所で滑るのか

AIが同じミスを量産するのは、注意力の問題ではありません。**構造の問題**です。だから注意ではなく機械で潰すのが効きます。

**構造1：プロンプトの字義実装。** 「ログインしたユーザーがメモを読めるようにして」と頼めば、AIも人も素直に「ログインしているか」を条件に書きます。`auth.role() = 'authenticated'` は、その日本語をほぼそのままSQLにした形です。「*本人の*行だけ」という言外の要件は、明示しない限り出力に現れません。認証（あなたは誰か）と認可（この行はあなたのものか）は日常語では地続きですが、SQLの述語では別物です。この境界そのものは[「認証はするのに認可しない」RLSの正しい直し方](/blog/supabase-rls-authenticated-vs-authorized-owner-scope-guide)で実SQLとともに詳説しているので、ここでは繰り返しません。

**構造2：デモで絶対に露見しない。** 開発中、自分は自分のアカウント1つしか使いません。自分のメモしか作っていないので、全件返ってきても「自分のメモが全部見える」だけで、正常に見えます。**2つ目のアカウントで他人のIDを叩く操作を、誰もしない**ため、テストは緑のまま本番へ行きます。RLSの穴は「敵対的な2人目」を用意して初めて見える。AIはその2人目を勝手には用意しません。

**構造3：モデルが賢くなっても変わらない。** 「賢いモデルなら安全に書くのでは」という期待は裏切られます。生成コードのセキュリティ評点はモデル世代を跨いでほぼ横ばい、という調査結果があります。AIは「動くコード」の生成には極めて強いが、「壊れない構造」を保証する主体ではない。だから、賢さに賭けるのではなく、[CIの品質ゲート](/blog/ai-driven-development-quality-gates-ci-type-safety-test-security)で機械的に検証するのが唯一の再現可能な解です。

この「認可という*縦のリスク*が、AIにも人にも『動いたからOK』で見過ごされる」構図は、OWASP Top 10で不動の1位である **A01:2021 Broken Access Control**([OWASP](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)) そのものです。RLSはこの認可を*データベース側で*書く場所であり、AIが最も滑りやすい場所でもあります。

---

## 2. AIが吐くRLSの定型ミス・カタログ

ここが本記事の中身です。AI（Claude Code / Cursor / v0 / Lovable / Bolt など）が生成しがちなRLSの失敗を、実SQLで並べます。まず一覧です。

| # | 定型ミス | AIがそう書く理由 | 見抜きの一手 |
|---|---|---|---|
| 2.1 | `USING (true)` の全開 | 「まず動かす」を字義通りに | 所有列があるのに全開か？ |
| 2.2 | `auth.role() = 'authenticated'` | 「ログインユーザーが読める」の直訳 | 述語が `user_id` を参照しているか？ |
| 2.3 | `WITH CHECK` 欠落 | SELECTだけ考えて書き込みを忘れる | INSERT/UPDATEに対の `WITH CHECK` があるか？ |
| 2.4 | RLS未有効化 / service_role誤用 | `CREATE POLICY` で満足、`ENABLE` を忘れる | `ENABLE ROW LEVEL SECURITY` があるか？鍵はどこ？ |
| 2.5 | `SECURITY DEFINER` の `search_path` 未固定 | 関数を作るが権限昇格面を知らない | 定義に `set search_path` があるか？ |

### 2.1 `USING (true)` — 全開ドア

最も直截な失敗です。「とりあえず読めるようにして」を字義通りに実装するとこうなります。

```sql
alter table public.invoices enable row level security;

-- 危険：所有列があるのに、全員に全行を開いている
create policy "read invoices" on public.invoices
  for select to authenticated
  using (true);
```

`USING (true)` は全行で真——`invoices` に `user_id` があっても一切参照せず、ログインユーザー全員に全請求書が返ります。ただし冒頭のFAQで書いたとおり、**`USING (true)` は共有マスタ（国一覧・カテゴリ・公開記事）なら正当**です。危険なのは「所有列があるのに全開」という組み合わせだけ。Aegisの言葉で言えば、これは「そのテーブルが*公開意図*なら `USING (true)` で構わない。所有列があるなら要確認」という**縦のリスク**であり、機械は「形」で警告できても「意図」までは判定できません。

### 2.2 `auth.role() = 'authenticated'` — 認証はするが認可しない

`USING (true)` を少しだけ賢くしたつもりの、最頻出パターンです。

```sql
-- 危険：ログイン済みかしか見ていない（＝全ログインユーザーに全行）
create policy "read notes" on public.notes
  for select to authenticated
  using (auth.role() = 'authenticated');
```

`auth.role()` はログイン中なら常に `'authenticated'`。`auth.uid() is not null` も同じで、セッションの存在証明にすぎず、`user_id` を一度も見ていません。正しい形は所有者への束縛です。

```sql
-- 安全：行を呼び出し元の所有に束縛
create policy "read notes" on public.notes
  for select to authenticated
  using ((select auth.uid()) = user_id);
```

`auth.uid()` を `(select auth.uid())` で包むのはSupabase公式推奨の性能テクニックで、関数を行ごとに再評価せず初期プランで一度だけ評価します([Supabase Docs](https://supabase.com/docs/guides/database/postgres/row-level-security))。なぜこれで全行が漏れるのか、共有テーブルとの見分け方、`curl` での再現までは[認証 vs 認可の正典記事](/blog/supabase-rls-authenticated-vs-authorized-owner-scope-guide)に譲ります。この漏洩クラスの現実の重さは、AI生成プラットフォームで不十分なRLSにより任意テーブルの読み書きが可能になった **[CVE-2025-48757](https://nvd.nist.gov/vuln/detail/CVE-2025-48757)**（CVSS 9.3 CRITICAL, CWE-863）が示しています。

### 2.3 `WITH CHECK` 欠落 — 読みは守れて書きが素通り

これはAIが特に踏みやすい。SELECT の `USING` は所有者に絞れても、**書き込み側の `WITH CHECK` を省く**のです。

```sql
-- USING は正しい。だが INSERT/UPDATE に WITH CHECK が無い
create policy "update own notes" on public.notes
  for update to authenticated
  using ((select auth.uid()) = user_id);   -- 見える行は本人だけ
  -- ← with check が無い！ 書ける値に制約が無い
```

`USING` は「どの行が見えるか」、`WITH CHECK` は「どの値で書けるか」を制御する**別の関門**です([PostgreSQL Docs](https://www.postgresql.org/docs/current/ddl-rowsecurity.html))。`WITH CHECK` を欠くと、見えている本人の行を、`user_id` を他人に書き換えて更新できる——所有権を*渡す*方向の書き込みバイパスが開きます。対で書くのが必須です。

```sql
create policy "update own notes" on public.notes
  for update to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);
```

`USING` と `WITH CHECK` の取り違え・欠落が具体的にどう悪用されるかは[書き込みバイパスの正典記事](/blog/supabase-rls-with-check-using-write-bypass-guide)にまとめています。

### 2.4 RLS未有効化 と service_role 境界の取り違え

`CREATE POLICY` を書いただけで満足し、`ENABLE ROW LEVEL SECURITY` を忘れる——ポリシは*存在するのに一切適用されない*状態です。

```sql
create table public.orders (id uuid primary key, user_id uuid, total int);
-- alter table ... enable row level security;  ← これが無いとポリシは無力
create policy "own orders" on public.orders
  for select using ((select auth.uid()) = user_id);
```

もう一つが**鍵の取り違え**。`service_role` キーはPostgreSQLの `BYPASSRLS` で動き、RLSを完全に無視します([Supabase Docs](https://supabase.com/docs/guides/database/postgres/row-level-security))。AIに「サーバーからデータを取って」と頼むと、安易に `service_role` クライアントをAPIルートやクライアント側に漏らす実装を書くことがある。そうなると**守りの境界はRLSではなく鍵の置き場所に移り**、RLSを何枚張っても意味がありません。この境界感覚は、コード側で所有権チェックを忘れる[IDOR/壊れた認可](/blog/nextjs-supabase-idor-broken-authorization-rls-detection-guide)と地続きで、RLS未有効化・`USING(true)`・`anon`への過剰GRANTといった定番は[設定ミス検出ガイド](/blog/supabase-rls-misconfiguration-detection-audit-guide)に修正SQL付きで整理しています。

### 2.5 `SECURITY DEFINER` の `search_path` 未固定

AIがヘルパ関数を生成するとき、`SECURITY DEFINER`（定義者権限で実行）を付けつつ `search_path` を固定しないことがあります。

```sql
-- 危険：search_path を固定していない SECURITY DEFINER
create function public.is_member(org uuid) returns boolean
  language sql security definer
as $$ select exists(select 1 from memberships where org_id = org and user_id = auth.uid()) $$;
-- ← set search_path = '' が無い
```

`SECURITY DEFINER` 関数は、呼び出し元が `search_path` を差し替えて、攻撃者が用意した同名オブジェクトを参照させる権限昇格の入口になり得ます。定義に `set search_path = ''`（またはスキーマ固定）を必ず添えます。

```sql
create function public.is_member(org uuid) returns boolean
  language sql security definer set search_path = ''
as $$ select exists(select 1 from public.memberships where org_id = org and user_id = (select auth.uid())) $$;
```

---

## 3. 「見抜けたつもり」の逆側 — AIが正しく書いた紛らわしい形

ここは誠実さのために外せません。**「定型に見える＝脆弱」ではない**。AIの出力を機械的に叩くと、*正しいのに紛らわしい*形を誤検知しがちです。私が自作OSSのスキャナを実世界のリポジトリで鍛えたとき、最大の学びはここでした——素朴な検出は本物の穴の2倍以上を「穴」と報告し、その大半は次のような**正しい述語**でした。

```sql
-- ① service_role ゲート（バックエンド専用）— 一般ユーザーは満たせない。穴ではない
using (auth.role() = 'service_role')

-- ② 参加者束縛（DMの送受信者）— 匿名(uid=null)は満たせない。認可済み
using ((select auth.uid()) in (sender_id, receiver_id))

-- ③ 性能ラッパ付きの所有者束縛 — Supabase公式推奨。正しい
using ((select auth.uid() as uid) = user_id)

-- ④ 型キャスト付きの所有者束縛 — 正しく絞っている
using (auth.uid()::text = user_id::text)
```

①は `service_role` という**制限的**なゲートで、`'authenticated'`（本物の穴）とは正反対。③はまさに[RLSの性能最適化](/blog/supabase-rls-performance-optimization-select-wrap-index-guide)で推奨される書き方そのものです。**推奨パターンを脆弱と報告するツールは、一度で信頼を失います。** だからスキャナは「`auth` という単語に反応する」曖昧な検出ではなく、「穴とは何か」を肯定的に定義し、匿名が満たせない述語は抑制するよう設計しなければなりません。この誤検知クラスをどう実データで0にしたかの詳細は[実態調査の手法](https://github.com/tomodahinata/aegis/blob/main/research/rls-precision-study/README.md)に公開しています。

---

## 4. これは特殊事例ではない — 実測1,000アプリの下限値

「うちのAIはそこまで雑じゃない」と思うかもしれません。数字で答えます。**公開Supabaseアプリ1,000件の一次調査で、RLSを持つアプリのうち約9.2%が『認証はするが行を所有者に絞らない』ポリシを持っていた（公開リポジトリ標本ゆえ下限値。所有者非スコープ＝即脆弱ではなく要確認）**。マイグレーションを公開GitHubにコミットしている——つまり相対的に丁寧な層——でこの水準です。母集団に入らない非公開・マイグレーション無しのアプリを含めれば、真の発生率はこれより悪い方向にしか動きません。

強調したいのは、この9.2%が「9.2%が侵害可能」ではなく「**9.2%に、設計意図の確認を要するポリシがある**」という意味だということ。煽らないことが数字を信頼に変えます。調査の母集団の選び方、116,662ポリシをどう解析したか、精度をどう `precision 1.0` に保ったかは[Supabase RLSセキュリティ実態調査](/blog/supabase-rls-security-field-study)にすべて公開しています。ここで再解説はしません——本記事の主張は「これはAIの標準的な出力傾向であって、例外ではない」という一点です。

---

## 5. 私の検証ワークフロー — 生成ループに検証ゲートを1枚

では実際にどう回すか。私のバイブコーディングは「生成の速度はそのまま、出荷の前に検証を1枚挟む」だけです。RLSに関しては3段構えです。

```text
[1] 生成    Claude Code / Cursor でいつも通りポリシを書かせる
                    │
[2] 貼るだけ  1本のポリシを即判定したい → ブラウザRLSチェッカー
                    │  正しい雛形が欲しい → RLSジェネレータ
                    ▼
[3] 全体scan  リポジトリ横断で漏れを洗う → npx @aegiskit/cli scan
                    │
[4] 回帰固定  直したら二度と戻さない    → pgTAP で allow/deny をCIに
```

**貼るだけ（インストール不要・SQLは外に出ない）。** 生成されたポリシを1本、その場で判定したいだけなら、ブラウザ内で完結する[RLSチェッカー](/aegis/rls-checker)が最速です。`using (auth.role() = 'authenticated')` を貼れば「認証のみ（要確認）」、`using ((select auth.uid()) = user_id)` を貼れば「所有者スコープ（OK）」と即座に分類します。判定は100%ブラウザ内で走り、SQLはどこにも送信しません。逆に、正しいポリシの雛形が欲しいなら[RLSジェネレータ](/aegis/rls-generator)で、テーブルと所有列を指定して `USING`/`WITH CHECK` 対の安全な形を生成できます。

**リポジトリ全体をscan。** `supabase/migrations/**.sql` を横断して§2の定型ミスを洗い出すなら、無料OSSの[Aegis](/aegis)で。

```bash
# インストール不要・設定不要。ローカルで走り、稼働中アプリには触れない（静的解析のみ）
npx @aegiskit/cli scan
```

これは実態調査で使ったのと同じ検出です。**Aegisが自動化するのは「水平の統制」（ヘッダー/CSP・レート制限・CSRF・秘密情報衛生）と、RLS述語の「形」の検証まで。認可/IDOR・テナント分離という「縦のリスク」は検出・警告にとどめ、修正はあなたの設計に返します。**「完全に守る」とは言いません——そう謳うツールこそ、「入れたから大丈夫」という最悪の油断を生みます。

**回帰固定。** 見つけて直したら、`aegis fix --format json` の修正計画をそのままコーディングエージェントに渡して直させ、pgTAPで allow/deny をCIに固定します。この「生成→検証→CI」の型は、AIに実装を任せつつ仕様の決定を人間が握る[Spec駆動開発](/blog/spec-driven-development-claude-code-ai-agent-production-workflow)の一部として運用しています。

---

## 6. 自動で測れる線、測れない線

最後に、ここを崩すと全部が嘘になります。**いかなる静的解析も、認可の「正しさ」を証明できません。** ツールが見るのは述語の“形”であって、あなたの業務ルールやデータモデルの“意味”ではない。

| 自動で測れる（機械化） | 人間の設計判断に残る（監査） |
|---|---|
| `USING (true)` / `auth.role()='authenticated'` の検出 | そのテーブルは本当に共有意図か |
| `WITH CHECK` 欠落・RLS未有効化 | テナント分離が設計として正しいか |
| `SECURITY DEFINER` の `search_path` 未固定 | 権限昇格・業務ロジックの破れの余地 |
| 所有列 vs 述語の突き合わせ | 多段モジュールを跨ぐ認可フロー |

scanが0件でも、それは「よくある定型ミスを踏んでいない」であって「認可が正しい」ではありません。0件は出発点で、ゴールではない。**「このテーブルは本当に全員に見せていいのか」の判断は、あなたのデータモデルを知る人間にしかできない。** どこまでが自動で守れ、どこからが監査の領域なのかは[監査の範囲](/blog/nextjs-supabase-security-audit-scope-when-needed-guide)で具体的に切り分けています。

---

## まとめ

AIが生成したRLSは信用できるか——答えは「そのまま信用してはいけないが、恐れる必要もない」です。失敗が少数の定型に収束すると分かっている以上、`USING(true)`・`authenticated` 全許可・`WITH CHECK` 欠落・RLS未有効化・`SECURITY DEFINER` の `search_path` 未固定を、生成のたびに機械で叩けばいい。まず[貼るだけのRLSチェッカー](/aegis/rls-checker)か `npx @aegiskit/cli scan` で自分の出力を測ってください。

そして忘れないでほしいのは——**ツールは「ここを確認せよ」と地図を描くだけで、「正しい」とは言えない**ということ。RLSが本当に*認可*になっているか、テナント分離が本当に効いているかを、データモデルを知る人間の目で確かめたいときは、既存アプリの[認可レビュー・監査](/aegis/audit)からご相談ください。速く作る力と、安全に作る力は、同じコインの裏表です。
