すべての解説
深刻度:重大データベースセキュリティ

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を完全にバイパスします。これがクライアントサイドのコードに入ってしまえば、書いたポリシーはすべて無意味になり、キーを手にした誰もがデータベースへの完全な読み書き権限を得ます。

直し方

RLSが無効なテーブルを見つける

Supabaseの SQL Editor で次のクエリを実行し、公開されている public スキーマのうち、まだRLSが無効なテーブルをすべて一覧表示します。ダッシュボードの Security Advisor も、同じ問題を「RLS disabled in public」として報告します。

select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;
RLSを有効にし、所有者のみのポリシーを書く

各テーブルで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 に対する専用のポリシーが必要です。

anon key で外部からテストする

攻撃者とまったく同じ方法で、つまり公開キーだけを使い、ユーザーセッションなしでAPIに問い合わせてください。公開するつもりのない行が返ってきたら、ポリシーが緩すぎます。

curl "https://<project>.supabase.co/rest/v1/profiles?select=*" \
  -H "apikey: <anon-key>" \
  -H "Authorization: Bearer <anon-key>"
service_role 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 なら重大な漏洩です。

あなたのアプリは大丈夫ですか?

AppSafeなら、この問題を含む数十項目を1回の無料スキャンでチェックできます。

アプリを無料でスキャン