Skip to main content
友田 陽大
Application-layer security
Supabase
RLS
セキュリティ
PostgreSQL
Next.js
アーキテクチャ設計

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

公開GitHubのSupabaseアプリ1,000件・116,662件のRLSポリシを静的スキャンした一次調査。RLSを持つ994件の9.2%が『認証はするが行を所有者に絞らない』ポリシを持っていた(下限値)。看板ルールはこの2倍規模でも precision 1.0 を維持し、走査自体が姉妹ルールの誤検知を炙り出して0に。auth.role()='authenticated'の落とし穴と自分のアプリの測り方まで、実データで解説します。

Published
Last updated
Updated
Reading time
18 min read
Author
友田 陽大
Share

最初に結論を述べます。公開GitHubにあるSupabaseアプリ1,000件・116,662件のRLSポリシを静的にスキャンしたところ、RLS(行レベルセキュリティ)を実装している994件のうち9.2%が、『認証はするが認可しない』ポリシ——ログインしてさえいれば誰でも全行を読める設定——を持っていました。 「RLSを有効にしたから安全」と思っていても、その有効化が 認可 になっていない、という落とし穴です。(これは先行する450件版が見つけた8.1%を、2倍規模で再現したものです。)

ただし本記事の主役は、この 9.2% という数字そのものではありません。**むしろ「この数字をどうやって信頼できるものにしたか」**です。理由は2つ。第一に、235件の所見すべてを実SQLで裏取りし precision 1.0(残存誤検知0) を確認しました——素朴な検出器はこのクラスを約2倍に過大報告し、その差の大半は誤検知です。第二に、この走査は 自分のスキャナ自身 を点検しました——匿名ユーザーには決して満たせないポリシで姉妹ルールが誤発火していたのを捕まえ、0に直したのです。本記事は、(1) 何をどう測ったか、(2) 結果と中核の設計ミス、(3) 誤検知を0に保つ方法(この走査が炙り出したものを含む)、(4) あなたのアプリを今すぐ測る方法、までを一次データで解説します。

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


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

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

  • 母集団: GitHubのコード検索で supabase/migrations または supabase/schemasCREATE POLICY を持つ公開リポジトリを 2,234件発見し、そこから 1,000件をシード付きランダム抽出で解析しました。この2ディレクトリは、Supabaseがスキーマの真実源とみなす場所です。
  • 解析対象: マイグレーションSQL(=権威あるスキーマ定義)のみ。静的解析だけで、デプロイ済みのアプリには一切アクセスしていません(後述の倫理)。
  • 指標の定義: 「RLSはあるが、行を所有者に絞らないポリシ」。具体的には、述語が auth.role() = 'authenticated'auth.uid() is not null のように 「セッションが存在すること」しか証明しておらず、auth.uid() = user_id のような所有権の束縛がない PERMISSIVE ポリシ。かつ、そのテーブルに所有者列(user_id 等)が存在するものだけを対象にしました。
指標
コード検索で発見した固有リポジトリ2,234
本調査でスキャンしたリポジトリ1,000
正常に解析できたリポジトリ998
RLSを実装(CREATE POLICY ≥ 1)994
解析したRLSポリシの総数116,662

本記事のすべての数字は、コミット済みで再現可能な成果物に紐づいています。調査の方法論・精度ハードニングの7イテレーション・実行スクリプトはすべて公開しており、ご自身で再実行できます。

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

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

1,000件はどう選んだか——そこに生じるバイアス

サンプルについて正直な caveat を2つ。第一に、1,000件は2,234件のプールからのシード付きランダム抽出です(先行する450件版はリポジトリ名順の先頭でしたが、本走査はそれとは独立した再現可能な無作為標本)。第二に、発見はキーワードベースで、母集団は一般的なRLSイディオムを含むリポジトリに偏ります。ただしそのキーワードの一部(auth.uid()to authenticated)は 正しい 所有者スコープのポリシにも現れる——つまり率を 下げる 方向にも効きます。そして分母は検索クエリではなく、クローンしたSQLに対する CREATE POLICY の実測 grep です。

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

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


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

結果です。RLSを持つ994件のうち、91件(9.2%)が、所有者に絞らないポリシを少なくとも1つ持っていました(該当ポリシは計235件、全116,662ポリシの約0.2%)。そして本記事の数字で最も重要な点——この235件は、1件ずつ実SQLで「本物の穴」だと確認済みです(誤検知0)。先行する450件版は8.1%でしたが、より大きく独立した標本でも同じ水準に着地しました。

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

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

-- 危険: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(CVSS 9.3 CRITICAL、CWE-863 Incorrect Authorization)は、AI生成プラットフォームの不十分なRLSポリシにより、未認証の攻撃者が任意のテーブルを読み書きできた実例です。本調査の9.2%は、その「一歩手前」——認証は要求するが、行を所有者に絞れていない——のポリシが、いかに普通に存在するかを示しています。

正しい形はこうです。

-- 安全:行を呼び出し元の所有に束縛する。
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 の取り違えによる書き込みバイパスで詳説しています。

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

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

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


3. なぜ数字を信頼できるのか — 素朴なスキャナは約2倍に過大報告する

ここからが、本調査で最も価値のある部分です。この検出器を最初に作ったとき、先行する450件版が「素朴な版はどれだけ過大報告するか」を正確に測らせてくれました。その450件を素朴な検出器で生で測ると 19.3%(86件・573所見) という、本当の率の2倍以上が出ます。その差は実力ではなく 誤検知 です。生の573所見のうち、最終的に本物だったのは99件——約83%が偽陽性でした。

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

このハードニング済みの分類器を、今回 1,000件・116,662ポリシ に対して走らせました。出てきた235件の所見は、再クローンによる手作業の裏取りで 全件が本物(2倍規模で precision 1.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_idSupabase公式が推奨する性能向上ラッパ(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性能最適化で推奨される書き方そのものを「穴」と誤報していました。推奨パターンを脆弱と報告するツールは、信頼を一度で失います。

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

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

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


4. 1,000件走査が、自分のスキャナの誤検知を捕まえた

ここは、多くの記事が見せない部分です。1,000件走査は単なる「測定」ではなく、スキャナ自身のストレステストでもありました——そして 姉妹ルール の誤動作を捕まえました。

rls/anon-writable は、匿名の訪問者が満たせてしまう書き込みポリシ(最も「実害」に近い)を検出します。1,000件では80件の所見が出ましたが、手作業のトリアージで 約68%(80件中54件)が誤検知 でした。匿名には決して満たせない次の2形を、分類器が「匿名が満たせる」と取り違えていたのです。

誤検知の形なぜ匿名は満たせないか
カスタム auth.*() ヘルパ — auth.is_admin()auth.email()auth.user_role()auth.org_id()プロジェクト定義のauth系関数で、セッション/ロール/同一性に依存。分類器は組み込み3種(auth.uid/jwt/role)しか知らず、残りを素の行条件と誤読していた
auth.role() IN ('service_role', 'supabase_admin')ロールゲートの リスト形式。分類器は auth.role() = '…' は認識するが IN (…) を取りこぼしていた

修正は、owner-scopeルールと同じ fail-secure の原則です——検証できない述語は、悪用可能と決めつけず抑制する。 未知の auth.*() ヘルパは「検証不能な委譲」として扱い、auth.role() IN (…) はロールゲートとして認識するようにしました。これで anon-writable80 → 25 に減り(残る25件は本物の匿名満たし得る述語)、看板ルールの再現率(recall)は無損失(同一標本の再走査で235件は不変)。両形とも回帰fixtureとして固定したので、CIの precision = 1.0 ゲートが二度と静かに取りこぼすことはありません。

これが「自分の精度を測り続ける」の実体です。 一度きりのベンチマークではなく、fixtureが持っていなかった形を実コードが出し続けるだけの大きさのコーパスで走らせ、新しい誤検知を1つずつ0に戻してから数字を出す。これを実データでできないスキャナは、チームに「無視する訓練」を静かに施します。


5. その他の信号(同じ精度では監査していない)

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

  • RLS未有効化のテーブルがある: 27.4%(273/998)。ENABLE ROW LEVEL SECURITY の付け忘れ。比較的「硬い」指標ですが、サンプル検証は要します。
  • 匿名で既存行を変更できる露出: 上記ハードニング後で数%。最も「実害」に近く、優先して精査する価値があります。

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


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

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

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

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

# .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セキュリティゲートを参照してください。


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

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

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

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

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


8. よくある質問(FAQ)

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

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

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

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

この9.2%は「脆弱なアプリの割合」ですか?

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

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

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

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

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

監査(有料の精査)はどんなときに必要ですか?

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


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

公開Supabaseアプリ1,000件・116,662ポリシのうち、RLSを持つ9.2%が「認証はするが認可しない」ポリシを持っていました。だがこの記事の本当の主張は、その数字を信頼に変えるには、過大報告を生む誤検知を実データで1つずつ潰し、さらにコーパスが大きくなって新しい形が出るたびに潰し続けるしかないということです。service_role、引用符付き識別子、型キャスト、性能ラッパ、カスタム auth.*() ヘルパ——「穴に見えるが穴ではない」形を区別できて初めて、数字は意味を持ちます。

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

友田

友田 陽大

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.

The vulnerabilities in this article — is your app safe from them?

An expert audit of your Next.js × Supabase authorization & RLS

The IDOR, RLS misconfigurations, and tenant-boundary crossing covered here are vertical risks a library can't fix. I take it on as a security audit — from authorization review through fix design and implementation. You're welcome to visualize the current state with the free OSS first.

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

Also worth reading