supabase-security-check
検証済みv1.0.0Supabase を使う Web ツールのデプロイ前に RLS・キー露出・アクセスパターンを点検し GO/CONDITIONAL/NO-GO を出す。「Supabaseのセキュリティチェック」「Supabase点検」「RLS確認」「デプロイ前Supabase監査」「supabase-security-check」等のトリガーで使用。汎用の /security-audit より軽量・Supabase専用。
Supabase セキュリティチェック
Supabase をバックエンドに使う Web ツールを本番/社内展開する前に、Supabase 側の設定起因の漏洩リスクを点検する。判定は GO / CONDITIONAL GO / NO-GO。断定より観測を優先し、各指摘に根拠(advisor lint 名・テーブル名)を添える。
前提条件
以下のいずれかで対象 Supabase プロジェクトにアクセスできること:
- Supabase MCP(推奨): Claude Code / claude.ai に Supabase 連携(MCP)が、対象プロジェクトを持つアカウントで接続済みであること(
list_organizationsで見えるか確認。別アカウントだと権限エラー)。本スキルの手順は MCP のツール名(list_projects/get_advisors/list_tables等)を前提に書かれている。 - Supabase CLI: MCP が使えない環境では
supabase login済みの Supabase CLI(supabase projects list/supabase inspect db等)で同等の情報を代替取得できる。手順中の MCP ツール呼び出しは CLI コマンドに読み替える。
対象プロジェクトの project_id を把握しておくこと(list_projects またはCLIで特定、もしくは利用者に確認)。
手順
1. プロジェクト特定
list_projects → 対象の project_id を確定(複数あれば利用者に確認)。
2. セキュリティ advisor
get_advisors(project_id, type="security") を実行し、lint を分類(下記「lint 解釈」)。remediation URL は利用者向けにそのまま提示する。
3. テーブル / RLS 状態
list_tables(project_id, schemas=["public"], verbose=false) で public スキーマの全テーブルを列挙し、RLS の有効/無効を確認。
4. アクセスパターン判定(最重要)
このツールが Supabase をどう読むかで、RLS の要否が真逆になる:
- 特権接続方式: サーバー(API)が
DATABASE_URL(postgres role)やservice_roleキーで読み書きする → RLS をバイパスする。この場合 RLS 有効・ポリシー無し = 正しい施錠(公開 API から全拒否)。 - anon キー方式: フロント/クライアントが
anon/sb_publishableキー + Supabase JS client で直接叩く → RLS ポリシーが必須。ポリシーが無いと「全拒否」でアプリが壊れる or 甘いポリシーだと漏洩。
見分け方: クライアント成果物に anon/publishable キー + createClient( があれば anon 方式。サーバーが接続文字列/service_role で叩くなら特権方式。
5. クライアント露出スキャン
ビルド成果物(dist/・バンドルJS)と設定ファイルを grep:
service_role/sb_secret… クライアントに出ていたら即 NO-GO(全 RLS をバイパスする最強キー)+ローテーション必須postgresql:/// 接続文字列 … クライアント/リポジトリにあってはならないanon/sb_publishable/eyJ(JWT) … anon 方式なら想定内。特権方式のツールに紛れていたら不要露出- 接続文字列(
DATABASE_URL)はソース/git に無く、ホスティングの env(暗号化)のみにあること
6. (任意)パフォーマンス
get_advisors(project_id, type="performance") で未使用インデックス等も見る(セキュリティ判定には含めない)。
lint 解釈(Single Source of Truth)
| lint | level | 意味 | 対応 |
|---|---|---|---|
rls_disabled_in_public | ERROR | public テーブルで RLS 無効 → anon キー + プロジェクトURL で誰でも読める | 原則 NO-GO。ALTER TABLE <t> ENABLE ROW LEVEL SECURITY;。anon 方式なら適切なポリシーも追加 |
rls_enabled_no_policy | INFO | RLS 有効・ポリシー無し | アクセスパターン次第。特権接続方式 → OK(意図した全拒否) / anon 方式 → ポリシー必須 |
security_definer_view | WARN | SECURITY DEFINER ビュー | 定義者権限で実行。意図確認、必要なら security_invoker |
function_search_path_mutable | WARN | 関数の search_path 未固定 | SET search_path = '' 等で固定 |
auth_*(leaked password 等) | WARN | Supabase Auth 利用時のみ | Auth 未使用なら N/A |
混同注意: 「RLS 有効・ポリシー無し(INFO)」は特権接続方式では"正しい"状態。ERROR の"RLS 無効"とは真逆。INFO を見て慌てて甘いポリシーを足すと逆に穴になる。
判定基準
IF service_role/接続文字列 がクライアントに露出: → NO-GO
ELIF rls_disabled_in_public が1件以上 (public にデータ有): → NO-GO(RLS有効化まで)
ELIF anonキー方式 かつ ポリシー未整備/過度に緩い: → NO-GO / CONDITIONAL
ELIF WARN級(search_path 等)が残る: → CONDITIONAL GO
ELSE(特権方式 + 全テーブルRLS有効 + キー露出なし): → GO
GO は「実施した Supabase 点検の範囲内でブロッカー無し」の意味。アプリ層の認証・ネットワーク・法令等は範囲外。
出力フォーマット
- 判定(GO/CONDITIONAL/NO-GO)を先頭に
- テーブル×RLS の一覧、advisor lint の分類(ERROR/WARN/INFO)
- アクセスパターン(特権 or anon)と、それに照らした RLS 妥当性
- クライアント露出スキャン結果
- 各指摘に remediation リンク
教訓(実案件で見つかったパターン)
- 特権接続方式のツールは「全テーブル RLS 有効・ポリシー無し」が正解(公開 API を全遮断し、アプリは接続文字列で動く)。
- 逆に RLS 無効は ERROR:anon キー + URL だけで公開 API からテーブルが読める。実案件で、同様の構成の別ツールがこの状態だったことがある(RLS 有効化を推奨)。
- 新規プロジェクトでも「RLS 有効化 + ポリシー無し + アプリは DATABASE_URL(postgres) 接続」を既定にすると堅い。