Supabase の Row Level Security(RLS):テーブルを確実に保護する
Supabaseは、Postgresのテーブルを Data API 経由でブラウザに直接公開し、公開用の anon(publishable)key で認証します。どのユーザーがどの行を読み書きできるかを決めるのが、Row Level Security(RLS)のポリシーです。public スキーマにあってRLSが無効なテーブルは、サイトを開いた誰もが読み取れ、多くの場合は書き込みまでできてしまいます。
これは何か
どのSupabaseプロジェクトにも、フロントエンドに置く前提の anon(または sb_publishable_)key があります。このキーが識別するのはプロジェクトであって、ユーザーではありません。誰でもJavaScriptバンドルからコピーし、UIを完全に迂回して https://<project>.supabase.co/rest/v1/ に自分でリクエストを送れます。
Row Level Security は、テーブルにポリシーを付与するPostgresの機能です。ポリシーとは、SELECT、INSERT、UPDATE、DELETE のたびに各行に対して評価されるSQLの条件です。RLSを有効にしてポリシーが1つもなければ、APIからは何もアクセスできません。ポリシーを1つ追加するごとに、扉がちょうど1つ開きます——たとえば「サインイン済みのユーザーは、user_id が自分のIDと一致する行を読み取れる」といった具合です。
なぜ重要か
ポリシーの欠落や広すぎるポリシーは、軽微なバグではなく完全なデータ漏洩です。ユーザーのメールアドレス、プロフィール、注文、プライベートメッセージ——テーブルにあるものは何でも——curlコマンド1つで抜き取れてしまいます。AIコーディングツールはSQLマイグレーションでテーブルを作成することが多く、その場合RLSは既定で無効です。さらに、アプリを動かすためだけに using (true) のような緩いプレースホルダーのポリシーを生成しがちです。
service_role(sb_secret_)key はRLSを完全にバイパスします。これがクライアントサイドのコードに入ってしまえば、書いたポリシーはすべて無意味になり、キーを手にした誰もがデータベースへの完全な読み書き権限を得ます。
直し方
Supabaseの SQL Editor で次のクエリを実行し、公開されている public スキーマのうち、まだRLSが無効なテーブルをすべて一覧表示します。ダッシュボードの Security Advisor も、同じ問題を「RLS disabled in public」として報告します。
select tablename
from pg_tables
where schemaname = 'public'
and rowsecurity = false;各テーブルでRLSを有効にし、操作ごとにポリシーを1つずつ追加します。ポリシーの対象は authenticated ロールに絞り、行の所有者カラムを auth.uid() と比較してください。INSERT と UPDATE には with check が必要です。これがないと、ユーザーが他人の行を書き込めてしまいます。
alter table public.profiles enable row level security;
create policy "Users read own profile"
on public.profiles for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "Users update own profile"
on public.profiles for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);using (true) のようなプレースホルダーのポリシーは、実際の条件に置き換えてください。user_metadata はユーザー自身が編集できるため、認可の根拠にしてはいけません。代わりに app_metadata かロール用のテーブルを使いましょう。ビューは security_invoker = true を指定して作成しない限りRLSをバイパスします。また、Storageのバケットには storage.objects に対する専用のポリシーが必要です。
攻撃者とまったく同じ方法で、つまり公開キーだけを使い、ユーザーセッションなしでAPIに問い合わせてください。公開するつもりのない行が返ってきたら、ポリシーが緩すぎます。
curl "https://<project>.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>"service_role(sb_secret_)key は、APIルートや Edge Functions などのサーバーコードでのみ使い、NEXT_PUBLIC_ や VITE_ のプレフィックスの後ろには決して置かないでください。一度でもブラウザに配信されたことがあるなら、ダッシュボードでローテーションしてください。
FAQ
Supabaseの anon key がブラウザから見えていても安全ですか?
はい。anon(publishable)key は公開される前提で設計されています。ただし、公開されているすべてのテーブルでRLSが有効になり、正しいポリシーが設定されている場合に限ります。キー自体は、ポリシーが許可していることしか許可しません。
RLSを有効にしたら、アプリが空の結果しか返さなくなりました。壊れたのでしょうか?
それはRLSが意図どおりに動いている証拠です。ポリシーがなければ、すべての行へのアクセスが拒否されます。RLSを再び無効にするのではなく、アプリに必要な操作に対して、適切なユーザーに絞ったポリシーを追加してください。
AppSafeはRLSのポリシーをテストしますか?
AppSafeはデータベースにログインしないため、ポリシーを評価することはできません。上記のSQLとcurlによるチェックを使ってください。ただし、JavaScript内のSupabaseのキーはスキャンします。キーが報告された場合は、JWTの中央部分をデコードして role クレームを確認してください。anon ならブラウザにあって問題ありませんが、service_role なら重大な漏洩です。