# AIで構築したアプリにおけるSupabaseの行レベルセキュリティのミス

Cyfendo · 2026年10月20日

AIで構築したアプリに最も多いSupabaseの行レベルセキュリティ（RLS）のミスは、RLSが無効なテーブル、すべての人を許可するポリシー、サインイン済みのユーザーなら誰でも許可するポリシー、ユーザーが編集できるデータに基づく認可です。その多くには共通の原因があります。クエリが権限エラーで失敗したとき、アプリを動かすための最小の変更はポリシーを緩めることであり、「データが読み込まれない」問題の修正を頼まれたAIエージェントは、まさにその変更をすることがよくあります。

これらのミスは、Lovable、Boltなどのツールで作られたアプリではより重大です。そうしたアプリには通常、ブラウザとデータベースの間にサーバーのコードがないためです。ブラウザは公開キーを使ってSupabaseにクエリを送り、各リクエストが到達できる範囲はRLSポリシーが決定します。ポリシーが欠けていると、1つのエンドポイントではなく、テーブル全体が露出します。

## 1. RLSが無効になっている

Supabaseのダッシュボードで作成したテーブルは、デフォルトでRLSが有効です。AIビルダーが使う方法であるSQLマイグレーションで作成したテーブルは、マイグレーションが有効にした場合にのみRLSが有効になります。RLSのないテーブルは、公開キーを持っている人なら誰でも読み取り、変更できます。これが[CVE-2025-48757](/blog/is-lovable-safe-for-production#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`ロールが更新できる列を制限してください。

```sql
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アプリのリリース前チェックリスト](/blog/how-to-secure-a-lovable-app#4-keep-secret-keys-out-of-the-browser)のステップ4で説明しています。

## ポリシーをテストする

1. テストユーザーAとBの2人を作成し、それぞれにデータを入れます。
2. サインアウトした状態で、公開キーだけを使って各テーブルをリクエストします。非公開のものは何も返ってこないはずです。
3. Bとしてサインインし、Aの行の読み取り、更新、削除と、Aが所有する行の挿入を試みます。
4. 一般ユーザーとして、自分の`is_admin`、`plan`などのフィールドの更新を試みます。
5. サインアウトした状態で、`/rest/v1/rpc/`配下の各関数を呼び出します。

スキーマやポリシーを変更するたびに繰り返してください。AIエージェントが構築を続けているアプリでは、それが1日に数回になることもあります。

## CyfendoがRLSをテストする方法

プロジェクトのSupabaseマイグレーションファイルが確認対象のコードに含まれている場合、Cyfendoは隔離されたサンドボックス内で代替のSupabaseを起動し、プロジェクトのRLS設定とポリシーを適用して、2人のテストユーザーを作成します。その後、ステップ2と3のリクエストを実行します。公開キーだけで行を読み取り、変更するリクエストと、一方のユーザーとしてもう一方のユーザーの行を読み取り、または変更するリクエストです。再現できた問題は修正手順とともに報告します。Cyfendoは所有者チェック、`true`ポリシー、`auth.role()`ポリシーを理解します。解析できないポリシーはアクセスを拒否するものとして扱うため、再現していない漏えいは報告しません。[Bolt](/docs/integration/bolt)や[Lovable](/docs/integration/lovable)に接続すると、エージェントがデータアクセスを変更したときに実行されます。

Source: https://cyfendo.com/ja/blog/supabase-row-level-security-mistakes
