AIで構築したアプリにおけるSupabaseの行レベルセキュリティのミス
AIで構築したアプリに最も多いSupabaseの行レベルセキュリティ(RLS)のミスは、RLSが無効なテーブル、すべての人を許可するポリシー、サインイン済みのユーザーなら誰でも許可するポリシー、ユーザーが編集できるデータに基づく認可です。その多くには共通の原因があります。クエリが権限エラーで失敗したとき、アプリを動かすための最小の変更はポリシーを緩めることであり、「データが読み込まれない」問題の修正を頼まれたAIエージェントは、まさにその変更をすることがよくあります。
これらのミスは、Lovable、Boltなどのツールで作られたアプリではより重大です。そうしたアプリには通常、ブラウザとデータベースの間にサーバーのコードがないためです。ブラウザは公開キーを使ってSupabaseにクエリを送り、各リクエストが到達できる範囲はRLSポリシーが決定します。ポリシーが欠けていると、1つのエンドポイントではなく、テーブル全体が露出します。
1. RLSが無効になっている
Supabaseのダッシュボードで作成したテーブルは、デフォルトでRLSが有効です。AIビルダーが使う方法であるSQLマイグレーションで作成したテーブルは、マイグレーションが有効にした場合にのみRLSが有効になります。RLSのないテーブルは、公開キーを持っている人なら誰でも読み取り、変更できます。これがCVE-2025-48757の原因であり、ある研究者は調査した1,645のLovableアプリのうち170でデータの露出を発見しました。
確認するには、pg_tablesでrowsecurityがfalseのpublicテーブルを一覧表示するか、SupabaseのSecurity Advisorを開いてください。
2. すべての人を許可するポリシー
selectポリシーのusing (true)は、テーブルを公開状態にします。ブログ記事や商品カタログのように、いずれにせよ公開するデータならそれで正しい設定です。それ以外のデータでは漏えいであり、空のページの修正として入り込むことがよくあります。マイグレーション内のusing (true)とwith check (true)を検索し、それぞれが意図したものであることを確認してください。
3. 「サインイン済み」を「許可済み」として扱う
using (auth.role() = 'authenticated')のようなポリシーは、テーブルをサインイン済みのユーザーに限定します。サインアップを誰にでも開放しているアプリでは、誰でもアカウントを作成してサインイン済みのユーザーになり、すべての行を読み取れます。ユーザーごとのデータを保護するポリシーは、行の所有者と呼び出し元を比較します。たとえば(select auth.uid()) = user_idのようにします。
4. 所有者を確認しない挿入
with check (true)を持つinsertポリシーは、別のユーザーの名義のメッセージや別の顧客のアカウントの注文など、他人に属する行をユーザーが作成できるようにします。各insertポリシーには、新しい行を呼び出し元に紐づけるwith checkが必要です。
5. RLSが列を保護すると期待する
RLSが制御するのは、ユーザーがどの行を操作できるかです。列は制御しません。ユーザーが自分のprofiles行を更新でき、その行にis_admin、plan、creditsが含まれていれば、ユーザーはブラウザからこれらのフィールドを変更できます。権限に関わるフィールドはユーザーが書き込めないテーブルに移すか、authenticatedロールが更新できる列を制限してください。
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
6. ユーザーメタデータに基づく認可
ユーザーは自分のuser_metadata(raw_user_meta_dataとして保存)を編集できます。そこからロールを読み取るポリシーや関数があると、ユーザーは自分自身を管理者にできます。Supabaseは、認可データにはapp_metadataを推奨しています。サーバーだけが変更できるためです。RLSで保護されたロール用テーブルでも構いません。
7. RLSをバイパスするビューと関数
- ビューは作成者(通常は
postgres)の権限で実行されるため、デフォルトでは基になるテーブルのRLSを無視します。Postgres 15以降では、with (security_invoker = true)を付けて作成してください。 - Security definer関数は作成者の権限で実行されます。公開されたスキーマにある場合、誰でも
/rest/v1/rpc/<name>で呼び出せます。CVE-2025-48757の調査では、あるアプリのRPC関数が、呼び出した人すべてにGemini APIキーを返していました。このような関数は公開されたスキーマの外に置くか、関数内で呼び出し元を確認してください。
8. ブラウザ内のシークレットキー
Supabaseのシークレットキー(sb_secret_...)とレガシーのservice_roleキーはRLSを完全にバイパスするため、どちらかがフロントエンドのコードに含まれると、ここまでのポリシーはどれも適用されません。ブラウザに置いてよいのは公開可能キー(またはレガシーのanonキー)だけです。各キーの見分け方と漏えい時の対処は、Lovableアプリのリリース前チェックリストのステップ4で説明しています。
ポリシーをテストする
- テストユーザーAとBの2人を作成し、それぞれにデータを入れます。
- サインアウトした状態で、公開キーだけを使って各テーブルをリクエストします。非公開のものは何も返ってこないはずです。
- Bとしてサインインし、Aの行の読み取り、更新、削除と、Aが所有する行の挿入を試みます。
- 一般ユーザーとして、自分の
is_admin、planなどのフィールドの更新を試みます。 - サインアウトした状態で、
/rest/v1/rpc/配下の各関数を呼び出します。
スキーマやポリシーを変更するたびに繰り返してください。AIエージェントが構築を続けているアプリでは、それが1日に数回になることもあります。
CyfendoがRLSをテストする方法
プロジェクトのSupabaseマイグレーションファイルが確認対象のコードに含まれている場合、Cyfendoは隔離されたサンドボックス内で代替のSupabaseを起動し、プロジェクトのRLS設定とポリシーを適用して、2人のテストユーザーを作成します。その後、ステップ2と3のリクエストを実行します。公開キーだけで行を読み取り、変更するリクエストと、一方のユーザーとしてもう一方のユーザーの行を読み取り、または変更するリクエストです。再現できた問題は修正手順とともに報告します。Cyfendoは所有者チェック、trueポリシー、auth.role()ポリシーを理解します。解析できないポリシーはアクセスを拒否するものとして扱うため、再現していない漏えいは報告しません。BoltやLovableに接続すると、エージェントがデータアクセスを変更したときに実行されます。