# Supabase RLSセキュリティ実態調査 — 公開アプリ450件中8.1%が『認証はするが認可しない』

> 公開GitHubのSupabaseアプリ450件を静的スキャンした一次調査。RLSを持つ445件の8.1%が『認証はするが行を所有者に絞らない』ポリシを持っていた。素朴なスキャナが19.3%と過大報告する誤検知をどう0にしたか、auth.role()='authenticated'の落とし穴と自分のアプリの測り方まで、実データで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Supabase, RLS, セキュリティ, PostgreSQL, Next.js, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/supabase-rls-security-field-study
- カテゴリ: アプリ層セキュリティ
- 総合ガイド: https://tomodahinata.com/blog/nextjs-supabase-application-security-guide

## 要点

- 公開GitHubのSupabaseアプリ450件を静的スキャン（公開ソースのみ・本番への探索は一切なし）。RLSを持つ445件のうち8.1%（36件・99ポリシ）が、ログイン済みなら誰でも全行を読める＝認証はするが行を所有者に絞らないポリシを持っていた
- 中核の誤りは `auth.role() = 'authenticated'` や `auth.uid() is not null` を『認可』と取り違えること。これは『ログインしているか』しか確かめておらず、所有者で絞っていない。RLSは有効でもテナント越え・他人のデータ閲覧が起きる
- ただし『所有者非スコープ＝即脆弱』ではない。公開カテゴリや共同編集など意図的に共有するテーブルは正当。だから検出はmedium・非ブロッキングの『意図を確認せよ』であって、煽りではない
- その8.1%が信頼できる最大の理由は、素朴なスキャナが19.3%と過大報告するのを正したから。生の所見573件の約83%は誤検知（`auth.role()='service_role'`＝バックエンド専用、引用符付き識別子、型キャスト等）で、実データで全クラスを潰して0にした
- 自分のアプリは `npx @aegiskit/cli scan` で測れる。ただしツールが自動で測れるのはここまで——認可の『正しさ』はいかなるツールも証明しない。修正は設計判断＝監査の領域

---

最初に結論を述べます。**公開GitHubにあるSupabaseアプリ450件を静的にスキャンしたところ、RLS（行レベルセキュリティ）を実装している445件のうち8.1%が、『認証はするが認可しない』ポリシ——ログインしてさえいれば誰でも全行を読める設定——を持っていました。** 「RLSを有効にしたから安全」と思っていても、その有効化が *認可* になっていない、という落とし穴です。

ただし本記事の主役は、この 8.1% という数字そのものではありません。**むしろ「この数字をどうやって信頼できるものにしたか」**です。同じスキャンを素朴に走らせると、最初は **19.3%** という、2倍以上に膨らんだ数字が出ます。その差——**生の所見の約83%——は誤検知**でした。本記事は、(1) 何をどう測ったか、(2) 8.1%という結果と中核の設計ミス、(3) なぜ多くのスキャナが過大報告し、その誤検知をどう実データで0にしたか、(4) あなたのアプリを今すぐ測る方法、までを一次データで解説します。

これは「Supabaseは危ない」という話でも、「AIを使うな」という話でもありません。**速く作ること自体は正しい。速く作ったRLSが、本当に *認可* になっているかを検証する**話です。アプリ層セキュリティの全体像は[Next.js × Supabase アプリケーションセキュリティ完全ガイド](/blog/nextjs-supabase-application-security-guide)に、個々のRLS危険パターンの修正は[Supabase RLSの設定ミス検出ガイド](/blog/supabase-rls-misconfiguration-detection-audit-guide)にまとめています。本記事はその主張を裏づける *実測* です。

---

## 1. 何を、どう測ったか — 手法と倫理

数字を出す前に、測り方を開示します。**測り方を隠した「N件中X%が脆弱」は、信用してはいけません。**

- **母集団**: GitHubのコード検索で `supabase/migrations` または `supabase/schemas` に `CREATE POLICY` を持つ公開リポジトリを **2,230件**発見し、そこから **450件**を解析しました。この2ディレクトリは、Supabaseがスキーマの真実源とみなす場所です。
- **解析対象**: マイグレーションSQL（＝権威あるスキーマ定義）のみ。**静的解析だけで、デプロイ済みのアプリには一切アクセスしていません**（後述の倫理）。
- **指標の定義**: 「RLSはあるが、行を所有者に絞らないポリシ」。具体的には、述語が `auth.role() = 'authenticated'` や `auth.uid() is not null` のように **「セッションが存在すること」しか証明しておらず、`auth.uid() = user_id` のような所有権の束縛がない** PERMISSIVE ポリシ。かつ、そのテーブルに所有者列（`user_id` 等）が存在するものだけを対象にしました。

| 指標 | 値 |
|---|---:|
| コード検索で発見した固有リポジトリ | 2,230 |
| 本調査でスキャンしたリポジトリ | 450 |
| RLSを実装（`CREATE POLICY` ≥ 1） | **445** |
| 解析したRLSポリシの総数 | 約 52,800 |

本記事のすべての数字は、コミット済みで再現可能な成果物に紐づいています。[調査の方法論・精度ハードニングの6イテレーション・実行スクリプト](https://github.com/tomodahinata/aegis/blob/main/research/rls-precision-study/README.md)はすべて公開しており、ご自身で再実行できます。

### 倫理：公開ソースの静的解析のみ

この種の調査でやってはいけないのは、**他人の本番エンドポイントに匿名でアクセスして「データが取れた」と確かめる**ことです。それは不正アクセスのグレーゾーンに踏み込みます。本調査は **公開リポジトリのSQLを読んだだけ**で、稼働中のアプリには触れていません。リポジトリ名も公開しません（集計値のみ）。日本の不正アクセス禁止法と「許可なき探索はしない」という原則は、[ホワイトハッカーの法律ガイド](/blog/ethical-hacker-law-japan-unauthorized-access-act-active-cyber-defense-disclosure-guide)でも触れているとおり、攻撃手法より先に守るべき土台です。

### 450件はどう選んだか——そこに生じるバイアス

サンプルについて正直な caveat を2つ。**第一に、450件はリポジトリ名順の先頭**です。これはRLSの設計とは無関係な順序なので恣意的サンプルとして扱いますが、*抽出されたランダムサンプル* ではありません。それを装うより、こう明記します（スクリプトは再実行向けにシード付きランダム抽出へ改修済み）。**第二に、発見はキーワードベース**で、母集団は一般的なRLSイディオムを含むリポジトリに偏ります。ただしそのキーワードの一部（`auth.uid()`、`to authenticated`）は *正しい* 所有者スコープのポリシにも現れる——つまり率を **下げる** 方向にも効きます。そして分母は検索クエリではなく、クローンしたSQLに対する `CREATE POLICY` の実測 grep です。

### 注意：これは「下限」であって「上限」ではない

公開GitHubに *マイグレーションをコミットしている* 開発者は、相対的に丁寧な層です。最も危ういアプリ——マイグレーションを残さない、リポジトリが非公開——はこの母集団に入りません。**つまり真の発生率は、この数字より良くなることはあっても、悪くなる方向に偏っている**と考えるのが妥当です。

---

## 2. 見出しの数字：8.1%が「認証はするが認可しない」

結果です。RLSを持つ445件のうち、**36件（8.1%）が、所有者に絞らないポリシを少なくとも1つ持っていました**（該当ポリシは計99件、全RLSポリシの約0.19%）。そして本記事の数字で最も重要な点——**この99件は、1件ずつ実SQLで「本物の穴」だと確認済み**です（誤検知0）。

### 中核の誤り：`auth.role() = 'authenticated'` は認可ではない

最も多かったパターンは、これです。

```sql
-- 危険：RLSは有効。だが「ログインしているか」しか確かめていない。
create policy "notes_read" on public.notes
  for select to authenticated
  using (auth.role() = 'authenticated');
```

`auth.role() = 'authenticated'` は、**呼び出し元がログイン済みであれば誰でも true** になります。ログインしたユーザー全員が、`notes` テーブルの **全行** を読めてしまう。`auth.uid() is not null`（セッションが存在することの確認）も同じ穴です。RLSは「有効」なのに、認可が効いていない。

これは机上の懸念ではありません。2025年の **[CVE-2025-48757](https://nvd.nist.gov/vuln/detail/CVE-2025-48757)**（CVSS 9.3 CRITICAL、CWE-863 Incorrect Authorization）は、AI生成プラットフォームの不十分なRLSポリシにより、未認証の攻撃者が任意のテーブルを読み書きできた実例です。本調査の8.1%は、その「一歩手前」——認証は要求するが、行を所有者に絞れていない——のポリシが、いかに普通に存在するかを示しています。

正しい形はこうです。

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

なお書き込み側（`WITH CHECK`）が「ログイン済み」しか見ていないと、`USING` が正しくても **他人のIDで行を作れる／`user_id` を他人に書き換えられる**（IDORライト）という別の穴になります。これは[USING と WITH CHECK の取り違えによる書き込みバイパス](/blog/supabase-rls-with-check-using-write-bypass-guide)で詳説しています。

### 誠実な但し書き：「所有者非スコープ＝即脆弱」ではない

ここを曖昧にする記事を信用してはいけません。**「ログイン済みなら全員が読める」ことが正しいテーブルは存在します。** 公開カテゴリ、共有マスタ、チームで共同編集するドキュメント——意図的に共有するテーブルなら、`authenticated` だけで読ませるのは正当な設計です。

だから、この検出は **medium（中）・非ブロッキング** の「**意図を確認せよ**」という警告であって、「脆弱だ」という断定ではありません。8.1%という数字は「8.1%が侵害可能」という意味ではなく、「**8.1%に、設計意図の確認を要するポリシがある**」という意味です。煽らないことが、数字を信頼に変えます。

---

## 3. なぜ8.1%を信頼できるのか — 素朴なスキャナは19.3%と過大報告する

ここからが、本調査で最も価値のある部分です。**同じ450件を、素朴な検出器で測ると 19.3%（86件・573所見）** という、2倍以上の数字が出ました。その差は実力ではなく **誤検知** です。生の573所見のうち、最終的に本物だったのは99件——**約83%が偽陽性**でした。

| 段階 | 該当リポジトリ | 所見数 | 内容 |
|---|---:|---:|---|
| 素朴な検出（生） | 86 / 445 = **19.3%** | 573 | このうち約83%が誤検知 |
| 誤検知クラスを実データで除去後 | 36 / 445 = **8.1%** | 99 | **全件、実SQLで裏取り（残存誤検知0）** |

「精度をベンチマークで測った」と主張するスキャナは多い。だが**キュレートされたテスト用fixtureでの "precision 1.0" は、実世界の精度を保証しません。** 実際のSupabaseリポジトリには、テスト用fixtureが想定していない「穴に見えるが穴ではない」形が大量にあります。主な誤検知クラスは次のとおりです。

| 誤検知の形（実例） | なぜ誤検知か | 生573所見に占める割合 |
|---|---|---:|
| `auth.role() = 'service_role'` | バックエンド専用ロール。RLSを迂回する側で、一般ユーザーは決して満たせない。**制限的**であって穴ではない | **46.8%** |
| `"auth"."role"() = 'authenticated'` | `pg_dump`／宣言的スキーマが出力する**引用符付き識別子**。素の `auth.role()` 前提の検出器は丸ごと取り違える | 多数 |
| `auth.uid()::text = user_id::text` | **型キャスト**付きの所有者束縛。正しく絞っているのに「`=` の左右が一致しない」と誤判定 | 多数 |
| `(select auth.uid() as uid) = user_id` | Supabase公式が推奨する**性能向上ラッパ**（CLIが自動生成する `as uid` 別名付き）。推奨パターンを誤検知 | 多数 |
| `auth.uid() in (sender_id, receiver_id)` | チャットの送受信者のような**参加者束縛**。匿名（uid=null）は満たせない＝所有者束縛 | 一部 |
| `current_setting('role') = 'service_role'` / `auth.jwt() ? 'service_role'` | `auth.*` 以外／JSONB演算子経由のロール判定。やはり匿名は満たせない | 一部 |

最大の塊である `service_role`——**生の所見の46.8%**——は、`auth.role()` という *文字列* に反応した検出器が、`'service_role'`（バックエンド専用）と `'authenticated'`（本物の穴）を区別できなかった結果です。`(select auth.uid() as uid) = user_id` に至っては、**Supabaseの[RLS性能最適化](/blog/supabase-rls-performance-optimization-select-wrap-index-guide)で推奨される書き方そのもの**を「穴」と誤報していました。推奨パターンを脆弱と報告するツールは、信頼を一度で失います。

これらのクラスを、**実データを使って1つずつ特定し、誤検知が0になるまで分類器を作り直しました**。判定の中核を「`auth.*` という単語に言及するか」という曖昧な検出から、**「穴とは何か」を肯定的に定義する**設計に変えています——穴は `auth.uid()/auth.jwt() IS NOT NULL` か `auth.role() = 'authenticated'` のみ。それ以外で `auth`／ロール／クレームに言及するもの（`service_role`、JWTクレーム、引用符付き識別子、`current_setting`）は、**匿名が満たせない＝穴ではない**として抑制します。

> **これが、監査人を選ぶときに見るべき一点です。** 「全部見つけます」と言うツールや人ではなく、**自分の誤検知に正直で、それを実データで潰せる**ツールや人を選ぶこと。偽陽性でノイズを出すスキャナは、3週間でチームに無視されます。

この精度ハードニングを支えているのが、無料OSSの **Aegis** です。スキャナの判定ロジックは、実世界の誤検知パターンを回帰テスト（fixture）として固定し、CIで `precision = 1.0` をハードフロアとして強制しています。「精度を実測した」という主張が、再現可能な成果物としてコミットされている——これが数字の裏付けです。

---

## 4. 副次的な発見（同じ精度では監査していない）

owner-scope以外にも、同じ450件で次の信号が出ました。**ただし、これらは8.1%ほど深く誤検知監査をしていない**ため、参考値として正直に提示します。

- **RLS未有効化のテーブルがある**: 28.6%（128/448）。`ENABLE ROW LEVEL SECURITY` の付け忘れ。比較的「硬い」指標ですが、サンプル検証は要します。
- **匿名で既存行を変更できる露出**: 数%。最も「実害」に近く、優先して精査する価値があります。

RLS未有効化や `USING (true)`、`anon` への過剰GRANT、`SECURITY DEFINER` 関数の `search_path` 未固定といった**5つの定番危険パターン**は、修正SQL付きで[Supabase RLSの設定ミス検出ガイド](/blog/supabase-rls-misconfiguration-detection-audit-guide)に、認可を跨ぐ[IDOR/壊れた認可の検出](/blog/nextjs-supabase-idor-broken-authorization-rls-detection-guide)は別記事にまとめています。

---

## 5. あなたのアプリを今すぐ測る

この記事を読んで「うちは大丈夫か？」と思ったら、**インストール不要・設定不要で30秒**で測れます。本調査と同じ検出を、あなたのリポジトリに対して走らせるだけです。

```bash
# プロジェクト直下で。supabase/migrations と TypeScript を静的解析する。
npx @aegiskit/cli scan
```

CIに組み込むなら、高信頼の所見でビルドを落とし、SARIFをGitHubのコードスキャンに上げられます。

```yaml
# .github/workflows/security.yml
name: Security
on: [push, pull_request]
jobs:
  aegis:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write # SARIF を Security タブへ
    steps:
      - uses: actions/checkout@v4
      - uses: tomodahinata/aegis@v1
        with:
          severity: HIGH
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: aegis.sarif
```

CI連携の詳細は[GitHub ActionsでのSARIFセキュリティゲート](/blog/nextjs-supabase-security-ci-sarif-github-actions-guide)を参照してください。

---

## 6. 何が自動で測れて、何が測れないか — 誠実なスコープ

ここは、本調査と同じくらい大事です。**いかなる静的解析も、認可の「正しさ」を証明できません**（一般には決定不能な問題です）。「全部見つける」と謳うツールは、その瞬間に最悪の結果——偽りの安心——を生みます。

ツールが自動で測れるのは、おおむねここまでです。

- **測れる（高信頼）**: 注入クラス（SQLi/SSRF/XSS）の「汚染入力→危険シンク」のデータフロー、RLSの述語が所有者に絞っているかの意味解析、ヘッダー/CSP・レート制限・CSRFの水平統制。
- **測れない（設計・監査の領域）**: 認可の業務的な正しさ（「この共有は *意図* どおりか」）、テナント越えの網羅、多段モジュールを跨ぐデータフロー、ビジネスロジックの破れ。

つまり、本調査の8.1%は「**ここに設計意図の確認が要る**」という地図であって、修正そのものではありません。**修正——『このテーブルは本当に全員に見せていいのか』の判断——は、あなたのデータモデルを知る人間にしかできない。** これが「検出・警告まで自動化、設計修正は人間」という線引きです。どこまでが自動で守れ、どこからが[監査が必要になるのか](/blog/nextjs-supabase-security-audit-scope-when-needed-guide)は、別記事で具体的に切り分けています。

---

## 7. よくある質問（FAQ）

### RLSを有効にすれば、Supabaseは安全ですか？

いいえ。RLSの「有効化」と「認可」は別物です。本調査では、RLSを実装した445件の8.1%が、`auth.role() = 'authenticated'` のように「ログインしているか」しか確かめず、行を所有者に絞っていませんでした。RLSは *枠* であって、*中身*（所有者で絞る述語）が認可です。

### `auth.role() = 'authenticated'` は何が問題ですか？

それは「呼び出し元がログイン済みか」しか確かめておらず、**ログインしたユーザー全員が全行を読める**ことを意味します。所有者で絞るには `using (auth.uid() = user_id)` のように、認証IDを所有者列に束縛します。ただし、意図的に共有するテーブル（公開マスタ等）では `authenticated` だけで読ませるのは正当です。

### この8.1%は「脆弱なアプリの割合」ですか？

違います。「所有者に絞っていないポリシを持つアプリの割合」です。そのうち何件が実際の脆弱性かは、**テーブルが共有を意図しているかどうか**——人間の設計判断——に依存します。だからこの検出は「脆弱だ」ではなく「意図を確認せよ」という非ブロッキングの警告です。

### 自分のアプリを無料で調べられますか？

はい。`npx @aegiskit/cli scan` をプロジェクト直下で実行すると、本調査と同じ検出が走ります。インストールも設定も不要で、デプロイ済みのアプリには一切アクセスしません（静的解析のみ）。

### スキャナの「精度が高い」という主張は信用できますか？

テスト用fixtureでの数字だけなら、信用しすぎないでください。本調査が示したとおり、素朴な検出は実世界で19.3%（うち約83%が誤検知）を出します。見るべきは「**実世界のリポジトリで誤検知をどう0にしたか**」が再現可能な形で開示されているかです。

### 監査（有料の精査）はどんなときに必要ですか？

ツールの自動検出が届かない領域——認可の業務的な正しさ、テナント越えの網羅、多段の認可フロー——を確かめたいとき、特に**エンタープライズ商談・調達のデューデリ・本番投入の直前**です。範囲の見極めは[セキュリティ監査の範囲ガイド](/blog/nextjs-supabase-security-audit-scope-when-needed-guide)を参照してください。

---

## まとめ：数字より、数字の作り方を信じる

公開Supabaseアプリ450件のうち、RLSを持つ8.1%が「認証はするが認可しない」ポリシを持っていました。だがこの記事の本当の主張は、**その数字を信頼に変えるには、19.3%という過大報告を生む誤検知を、実データで1つずつ潰すしかない**ということです。`service_role`、引用符付き識別子、型キャスト、性能ラッパ——「穴に見えるが穴ではない」形を区別できて初めて、数字は意味を持ちます。

まず `npx @aegiskit/cli scan` で自分のアプリを測ってください。そして覚えておいてほしいのは——**ツールは「ここを確認せよ」と地図を描くだけで、「正しい」とは言えない**ということ。RLSが本当に *認可* になっているか、テナント分離が本当に効いているかを、データモデルを知る人間の目で確かめたいときは、[Next.js × Supabase アプリケーションセキュリティ](/blog/nextjs-supabase-application-security-guide)の全体像と、[監査の範囲](/blog/nextjs-supabase-security-audit-scope-when-needed-guide)からご相談ください。速く作る力と、安全に作る力は、同じコインの裏表です。
