// docs / baas security / supabase rls scanner
Supabase RLS スキャナ: 行レベルセキュリティが欠落または破損したテーブルを検出
Supabase をバックエンドにしたアプリをリリースしたとき、顧客のデータとインターネットの間に立ちはだかる唯一のものが行レベルセキュリティ (RLS) です。AI コーディングツールは、コンパイルが通り、デプロイされ、しかし静かにデータを漏洩させる RLS 風コードを生成します — RLS を有効化せずに作成されたテーブル、読み取りはできるが制限しないポリシー、列自体を比較する述語などです。本記事では、Supabase RLS スキャナが外部から何を証明できるか、バイブコードされたアプリに現れる 4 つの破損 RLS パターン、そして自分のデプロイ環境を 1 分以内にスキャンする方法を示します。
外部 RLS スキャンが証明できること
パッシブな RLS スキャンは、Supabase が https://[project].supabase.co/rest/v1/ で公開している PostgREST エンドポイントに対して実行されます。使用するのは公開可能な anon キー — ブラウザが使うのと同じキー — のみで、テーブル一覧メタデータ、匿名読み取り、匿名書き込みを試行します。ユーザーとして認証することはなく、サービスロール権限に触れることもありません。スキャナが実行できることは、インターネット上の未認証攻撃者にも実行可能です。
データベース外部から、スキャナは以下を高い信頼度で確認できます:
- テーブルで RLS が無効化されている。 RLS がオフのとき、またはポリシーが許可しているとき、PostgREST は匿名
SELECTに対して行を返します。どちらの場合も検出対象です。 - 匿名ロールがテーブルを列挙できる。 anon キーを使った
GET /rest/v1/は、anonロールが何らかの権限を持つすべてのテーブルの OpenAPI スキーマを返します。AI 生成アプリはスキーマに対してUSAGEを、各テーブルに対してSELECTを付与することが多く、RLS が実際の読み取りを拒否していてもスキーマ全体のマップが露出します。 - 匿名ロールが INSERT できる。 列の形を推測した
POSTによる試行は、RLS にINSERTを拒否するポリシーがなければ — たとえSELECTがロックダウンされていても — 成功します。 - サービスロールキーがブラウザバンドルに含まれている。 RLS に隣接する問題: スキャナが JavaScript バンドル内で
SUPABASE_SERVICE_ROLE_KEYまたはrole: service_roleを含む JWT を発見した場合、RLS は意味を失います — そのキーの保有者はあらゆるポリシーをバイパスします。
外部スキャンが証明できないこと
スキャナの境界について正直であるべきです。外部 RLS スキャンは、あなたの pg_policies テーブル、マイグレーションファイル、または特定ポリシーの厳密な述語を読むことはできません。ブラックボックスの挙動から推測するため、意図的に公開しているデータ (マーケティングニュースレターのテーブル、公開製品カタログなど) を 検出 として報告することがあります。FixVibe レポートは、スキャナが意図を判別できない場合は 中程度の信頼度 としてフラグを立てます — テーブル名を確認して判断してください。
AI ツールが生成する 4 つの破損 RLS パターン
Cursor、Claude Code、Lovable、Bolt を Supabase に向けると、何千ものアプリで同じ 4 つの破損 RLS パターンが現れます。それぞれが型チェックを通過し、コンパイルされ、デプロイされます:
パターン 1: RLS が一度も有効化されていない
最も一般的な失敗モードです。マイグレーションはテーブルを作成しますが、開発者 (または AI ツール) が ALTER TABLE ... ENABLE ROW LEVEL SECURITY を忘れます。PostgREST は anon キーを持つ誰にでもテーブル全体を喜んで提供します。修正: ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY; ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;。FORCE は必須です — これがないとテーブル所有者 (およびテーブル所有権を持つ任意のロール) は RLS をバイパスします。
ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;パターン 2: RLS は有効、ポリシーなし
より微妙な失敗。RLS は有効ですが、ポリシーが書かれていません。PostgreSQL のデフォルトは 拒否 なので、認証済みユーザーには何も見えません — そこで開発者は USING (true) を追加してアプリを動かそうとし、結果として誰もがすべてを読めるようになります。修正: auth.uid() でスコープを絞ったポリシーを書きます: CREATE POLICY "select_own" ON public.[name] FOR SELECT USING (auth.uid() = user_id);、加えて対応する INSERT/UPDATE/DELETE ポリシー。
CREATE POLICY "select_own"
ON public.[name]
FOR SELECT
USING (auth.uid() = user_id);パターン 3: ポリシーが列を自分自身と比較する
A copy-paste artefact. The developer writes <code>USING (user_id = user_id)</code> — which is always true — instead of <code>USING (auth.uid() = user_id)</code>. Type-checks pass; the policy permits every row. <strong>Fix:</strong> always compare a column to a function call (<code>auth.uid()</code>, <code>auth.jwt()->>'org_id'</code>, etc.), never to itself or to a constant.
パターン 4: SELECT には適用されるが INSERT/UPDATE には適用されないポリシー
開発者は読み取りをロックしますが、書き込みを忘れます。RLS ポリシーはコマンドごとです。FOR SELECT は読み取りのみを保護します。拒否するポリシーがなければ、匿名クライアントは INSERT できてしまいます。修正: コマンドごとにポリシーを記述するか、明示的な USING と WITH CHECK 句を伴う FOR ALL を使用します。
FixVibe Supabase RLS スキャナの仕組み
baas.supabase-rls チェックは 3 つのステージで実行され、それぞれに明示的な信頼度レベルが付与されます:
- ステージ 1 — フィンガープリント。 スキャナはデプロイされたアプリをクロールし、JavaScript バンドルを解析し、ランタイム設定から Supabase プロジェクト URL と anon キーを抽出します。DNS の推測やブルートフォースは行いません — ブラウザが読むものを読むだけです。
- ステージ 2 — スキーマ発見。 anon キーを使った 1 回の
GET /rest/v1/で、anon ロールが見えるすべてのテーブルの OpenAPI スキーマが返されます。スキャナはテーブル名を記録しますが、この段階では行データを読みません。 - ステージ 3 — 読み取りおよび書き込みプローブ。 発見された各テーブルに対し、スキャナは
limit=1を伴う匿名SELECTを 1 回発行します。行が返れば RLS は許容的です。スキャナはそこで止まります — 行を列挙したり、ページングしたり、データを変更したりはしません。INSERT プローブは検証済みドメイン所有権と明示的なオプトインの背後でゲートされ、未検証のターゲットに対しては決して発火しません。
各検出結果には、正確なリクエスト URL、レスポンスステータス、レスポンス形状 (ヘッダのみ)、テーブル名が含まれます。検出結果の下部にある AI 修正プロンプトは、Supabase の SQL エディタで実行できるコピー&ペースト可能な SQL ブロックです。
スキャナが何かを検出したときの対応
すべての RLS 検出はランタイムの緊急事態です。公開 PostgREST エンドポイントは数分以内に攻撃者にスキャンされます。修復シーケンスは機械的です:
- すべてのテーブルを監査。 Supabase SQL エディタで
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';を実行します。rowsecurity = falseの行はすべて問題です。 - すべての public テーブルで RLS を有効化。 作成するすべてのテーブルで
ENABLE ROW LEVEL SECURITYとFORCE ROW LEVEL SECURITYをデフォルトにし、マイグレーションテンプレートに組み込みます。 - コマンドごとにポリシーを作成。
FOR ALL USING (true)を使ってはいけません。SELECT、INSERT、UPDATE、DELETE それぞれに明示的なポリシーを書き —auth.uid()やauth.jwt()から取得した組織 ID 列でスコープを絞ります。 - 別アカウントで検証。 別のユーザーとしてサインアップし、REST API 経由で他のユーザーのレコードを直接読み取ろうとします。レスポンスが
200なら、ポリシーが壊れています。 - 再スキャン。 修正を適用したら、同じ URL に対して FixVibe スキャンを再実行します。
baas.supabase-rlsの検出結果はクリアされるはずです。
-- Audit every table for missing RLS. Run in the Supabase SQL editor.
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;他のスキャナとの比較
ほとんどの汎用 DAST ツール (Burp Suite、OWASP ZAP、Nessus) は PostgREST を理解しません。アプリをクロールし、/rest/v1/ パスを無視し、理解できる HTML ページについて報告するだけです。Snyk と Semgrep は静的解析ツールです — リポジトリ内の RLS 呼び出しが欠落したマイグレーションファイルを発見しますが、デプロイされたデータベースが設定ミスを起こしていることを証明できません。FixVibe はその隙間に位置します: パッシブ、BaaS を理解し、公開 URL から未認証攻撃者が証明できることに焦点を当てています。
よくある質問
スキャナは私のデータを読んだり変更したりしますか?
いいえ。パッシブスキャンは、発見された各テーブルに対し、RLS が匿名読み取りを許可するかを確認するために最大 1 回の SELECT ... limit=1 を発行します。スキャナはレスポンス形状を記録するだけで、行の内容は記録しません。INSERT、UPDATE、DELETE プローブは検証済みドメイン所有権の背後でゲートされ、未検証のターゲットに対しては実行されません。
Supabase プロジェクトが一時停止中だったり、カスタムドメインを使用している場合でも動作しますか?
一時停止中のプロジェクトはすべてのリクエストに 503 を返します — スキャナはプロジェクトを到達不能と報告します。デプロイされたアプリがブラウザで依然として Supabase クライアント SDK を読み込んでいる限り、カスタムドメインでも動作します。スキャナはどちらの場合もバンドルからプロジェクト URL を抽出します。
anon キーがローテーションされた場合や、公開可能キーが変更された場合はどうなりますか?
スキャンを再実行してください。スキャナは毎回現在のバンドルからキーを再抽出します。ローテーションは以前のレポートのみを無効化し、データベースのポリシー状態には影響しません。
スキャナは新しい Supabase 公開可能キーモデル (<code>sb_publishable_*</code>) もチェックしますか?
はい。検出器はレガシーの anon JWT と新しい sb_publishable_* キーの両方を認識し、同じように扱います — どちらも公開を想定しており、どちらも RLS を唯一の防御線として残します。
次のステップ
本番 URL に対して無料の FixVibe スキャンを実行してください — baas.supabase-rls チェックは無料プランを含むすべてのプランで有効です。Supabase プロジェクトから他に何が漏洩しうるかについてのより深い解説については、JavaScript に露出した Supabase サービスロールキーとSupabase ストレージバケットセキュリティチェックリストを参照してください。すべての BaaS プロバイダ横断の全体像についてはBaaS 設定ミススキャナをお読みください。
