Supabase と Firebase のアクセスルール(CWE-862):Row Level Security の欠如がデータを露出させる仕組みと直し方
Supabase や Firebase のアプリでは、ブラウザが公開キーでデータベースを直接問い合わせるため、訪問者とすべての行の間にあるのは Row Level Security か Security Rules だけです。enable row level security なしで SQL から作ったテーブルや、allow read, write: if true のままの Firestore ルールは、誰でも読み書きできます。USING と WITH CHECK のポリシーで行をログイン中のユーザーに限定する方法を、PostgreSQL のマイグレーション例で解説します。
わかりやすい解説 (ELI5)
あるオフィスビルでは、来訪者全員にパンフレットの裏に印刷された同じロビー用カードを渡しています。各階の扉が開く前に本人確認をしている限り、それで問題はありません。ところが急いで準備した階の扉にはカードリーダーがなく、ロビーのカードで開いてしまい、誰でも入って書類を読めます。Supabase の anon キーはこのロビー用カードで、公開される前提のものです。Row Level Security は各階の扉のカードリーダーです。対策は、すべての扉にリーダーを付け、誰がどの書類を見てよいかを正確に教えることです。
主要な概念と専門用語
- 公開の anon キー
- Supabase の
anonキーと Firebase の Web 設定はアプリのバンドルに含まれ、誰でもコピーできます。これらはユーザーではなくプロジェクトを識別するもので、秘密ではなく、それだけではデータを守れません。 - Row Level Security(RLS)
- すべてのクエリをポリシーで絞り込む PostgreSQL の機能です。Supabase の Table Editor は新しいテーブルで有効にしますが、SQL やマイグレーションで作ったテーブルは RLS がオフの状態で始まり、既定の権限により
anonとauthenticatedがすべての行を読み書きできてしまいます。 - USING と WITH CHECK
USINGは、ユーザーが参照・更新・削除できる既存の行を決め、WITH CHECKは、書き込める新しい行や変更後の行を決めます。insert や update のポリシーにUSINGしかないと、別の組織の行を書き込めてしまいます。- service_role キー
service_roleキーは RLS を完全に迂回します。サーバーやエッジ関数だけで使い、ブラウザやモバイルアプリには決して入れません。- Firestore のテストモードのルール
- テストモードで始めたプロジェクトには、ある日付まですべての読み書きを許すルールが入り、その後
allow read, write: if trueに書き換えられることもよくあります。ルールではrequest.auth.uidをドキュメントの所有者フィールドと照合する必要があります。
ステップ・バイ・ステップの攻撃フロー
攻撃者がアプリを開く
Supabase の URL と anon キーは JavaScript のバンドルやモバイルアプリのパッケージに入っています。
テーブルを直接問い合わせる
アプリの画面を通さず、anon キーだけで invoices の REST エンドポイントを呼び出します。anon キーだけを使いユーザーセッションなしでテスト用プロジェクトに送るリクエストは、この場合を示しています。
データベースがすべての行を返す
RLS がオフで、anon に select 権限があるため、PostgreSQL は何も絞り込まず全組織の請求書を返します。
データが漏れたり書き換えられたりする
update と delete も許可されていれば、攻撃者はレコードを変更・削除できます。2025 年、ある AI アプリビルダーが生成した 170 を超えるアプリで RLS の欠如によりデータが露出していることを研究者が見つけ、CVE-2025-48757 が割り当てられました。
ソースコード比較:脆弱 vs 堅牢化
-- migration.sql: このテーブルでは RLS が一度も有効にされない
create table public.invoices (
id bigint generated always as identity primary key,
org_id uuid not null references public.orgs (id),
customer text not null,
amount_cents bigint not null
);
-- 公開の anon キーですべての行を読み、変更し、削除できる
grant select, insert, update, delete on public.invoices to anon, authenticated;
-- migration.sql: RLS を有効化、anon の権限を取り消し、コマンドごとにポリシー
create table public.invoices (
id bigint generated always as identity primary key,
org_id uuid not null references public.orgs (id),
customer text not null,
amount_cents bigint not null
);
-- RLS が有効なら、ポリシーがない操作はすべて拒否される
alter table public.invoices enable row level security;
revoke all on public.invoices from anon;
-- ユーザーは自分が所属する組織の請求書だけを見られる
create policy "members read their org's invoices"
on public.invoices for select to authenticated
using (org_id in (select org_id from public.memberships where user_id = (select auth.uid())));
-- 新しい行はユーザーの組織のものに限る。update と delete のポリシーはなし
create policy "members add invoices to their org"
on public.invoices for insert to authenticated
with check (org_id in (select org_id from public.memberships where user_id = (select auth.uid())));
エンジニアリング&システム堅牢化チェックリスト
- 公開スキーマのすべてのテーブルで RLS を有効にし、
select tablename from pg_tables where schemaname = 'public' and not rowsecurityが 1 行でも返したら CI を失敗させる。 - コマンドごとに
to authenticatedで限定したポリシーを書き、読み取りにはUSING、insert と update にはWITH CHECKを使う。不要な update や delete のポリシーは作らない。 - 匿名ユーザーが読む理由のないテーブルでは
anonの権限を取り消し、service_roleキーはサーバー環境だけに置く。 membershipsのようにポリシーが参照するテーブルでも RLS を有効にし、ユーザーが自分を別の組織に追加できないようにする。- 2 人のテストユーザーで確認し、ユーザー A がユーザー B の組織の行を 0 件しか取得できないことを確かめる。Firestore では
@firebase/rules-unit-testingでエミュレーターに対してルールの単体テストを実行する。