「ChatGPTに聞いたら『RLSを有効にしろ』と言われた。でも意味も、やり方も分からない」
この記事は、その状態から抜け出すためのものです。コードは読めなくて構いません。仕組みが分かれば、あとはAIに指示を出せば直せます。
1. RLS とは何か — 表計算で考える
RLS(Row Level Security、行レベルセキュリティ)は「データベース側で、誰がどの行を見られるかを決める設定」です。
「行」というのは、表計算ソフトの1行だと思ってください。たとえば、あなたのアプリに「メモ」という表があるとします。
| id | user_id | 内容 |
|---|---|---|
| 1 | Aさん | 銀行の暗証番号のメモ |
| 2 | Aさん | 買い物リスト |
| 3 | Bさん | 転職活動のメモ |
RLS が設定されていれば、Aさんがログインしているとき、データベースは 1 と 2 の行しか返しません。3 は存在しないかのように扱われます。
RLS が設定されていなければ、誰が問い合わせても3行すべてが返ります。
ポイントは、この判定が データベース側で行われる ことです。アプリのコードに「Aさんの分だけ表示する」と書くのとは、防御の階層が違います。
2. なぜログイン画面があっても守られないのか
ここがいちばん直感に反する部分です。
「ログインしないと使えないアプリなんだから、ログインしていない人はデータを見られないはずでは?」——そう思うのが自然です。
実際に起きていること
Supabase のようなサービスでは、ブラウザからデータベースへ直接問い合わせが飛びます。あなたのアプリのコードを経由せず、です。
【あなたが想定している流れ】
ユーザー → ログイン画面 → あなたのアプリ → データベース
【実際にあり得る流れ】
攻撃者 ──────────────────────────────→ データベース
なぜ直接飛ばせるかというと、接続に必要な情報が どちらも公開されているから です。
- データベースのURL — ブラウザの開発者ツールで見える
anon(匿名)キー — 同上
そして、これは異常ではありません。anon キーは、設計上「公開されるもの」です。公開されることを前提に作られています。
では何が安全性を担保しているのか。それがRLSです。 anon キーは「データベースに話しかける権利」でしかなく、「何を見られるか」はRLSが決める——そういう設計になっています。
だからRLSが無いと、公開キーだけで全部が取れてしまう。鍵が漏れたのではなく、最初から鍵をかけていなかったというのが正確な表現です。
例えるなら
- ログイン画面 = 建物の玄関の鍵
- RLS = 各部屋の金庫の鍵
玄関に鍵をかけても、窓が開いていて、しかも金庫に鍵がかかっていなければ、中身は持っていかれます。Supabase の構成では「窓」が最初から開いている(それが正常な設計)ので、金庫の鍵が必須になります。
3. これは実際に起きている — CVE-2025-48757
これは理論上の話ではありません。
AI アプリビルダーの Lovable で生成されたアプリについて、次の内容の脆弱性が CVE として登録されています。
生成されたサイトのデータベースの行レベルセキュリティポリシーが不十分で、認証されていない遠隔の攻撃者が任意のデータベーステーブルを読み書きできる
深刻度は CVSS 9.3 CRITICAL。10点満点で9.3、最も深刻な部類です。
注目すべきは、ベンダーの反論
この CVE には、ベンダー側から異議申し立てが出ています。その主張の内容が重要です。
ベンダーの立場は「アプリのデータ保護は利用者の責任である」というものでした。
これは責任逃れというより、事実として正しい指摘です。そしてそれが意味するのは——あなたが作ったアプリのデータを守る責任は、あなたにあるということです。
「AIツールが自動でやってくれているはず」という期待は、公式に否定されています。ここは自分で確認するしかありません。
4. 有効にしただけでは足りない — USING (true) の罠
RLS を有効にすると、多くの場合アプリが動かなくなります。
これは正常です。RLS は「ポリシー(許可の条件)が1つも無ければ、全部拒否」という設計だからです。有効にした瞬間、データが1行も返ってこなくなります。
ここで起きるのが、この記事でいちばん伝えたい事故です。
アプリが動かなくなって焦り、AIに「データが取れなくなった、直して」と頼む。するとAIは、動く状態に戻すための最短の答えを出します。
-- AI が「動くようにする」ために書きがちな条件
CREATE POLICY "..." ON メモ FOR SELECT USING (true);
USING (true) は「常に許可」という意味です。つまり「全員が全行を見てよい」。アプリは動きます。そしてセキュリティ上は、RLSを無効にしていたのと結果が同じです。
RLS が「有効」と表示されているので、確認したつもりになってしまう。これが最も危険なパターンです。
正しい条件
本人に絞るには、こう書きます。
CREATE POLICY "自分のメモだけ読める" ON メモ
FOR SELECT
USING ( (select auth.uid()) = user_id );
auth.uid() は「いま問い合わせている人のID」です。それが行の user_id と一致するときだけ許可する——これが「本人の行だけ」の意味になります。
(select auth.uid()) という書き方をしているのは性能上の理由です。裸の auth.uid() だと行ごとに評価されてしまい、行数が増えたときに極端に遅くなります。Supabase の公式ドキュメントでも推奨されている書き方です。
5. 最頻出の抜け — 書き込みを忘れる
読み取りの制限を書いて、書き込みの制限を忘れる。これが実務でいちばんよく見る抜けです。
USING→ 「どの行を 見られる/触れる か」WITH CHECK→ 「どんな値の行を 作れる/変更後の値にできる か」
USING だけ書いた状態だと、こうなります。
- 他人の
user_idを指定したメモを、勝手に作れる - 自分のメモの
user_idを他人に書き換えて、押し付けられる
どちらも「読めない」だけで「書ける」ので、動作確認では気づきません。
正しい形
-- 作成: 自分名義でしか作れない
CREATE POLICY "自分のメモだけ作れる" ON メモ
FOR INSERT
WITH CHECK ( (select auth.uid()) = user_id );
-- 更新: 自分の行だけ触れて、かつ自分名義のまま
CREATE POLICY "自分のメモだけ更新できる" ON メモ
FOR UPDATE
USING ( (select auth.uid()) = user_id )
WITH CHECK ( (select auth.uid()) = user_id );
UPDATE に両方必要なのは、「どの行を触れるか」と「どんな値に変えられるか」が別の問いだからです。片方だけだと、自分の行を他人のものに書き換えられます。
6. 自分で確認する手順
コードを読まずにできる確認です。
手順1: 管理画面で有効/無効を見る
- Supabase の管理画面にログイン
- 左メニューの Table Editor を開く
- テーブル一覧を見る
「RLS disabled」「Unrestricted」といった警告バッジが出ているテーブルがあれば、それは全公開のテーブルです。
手順2: ポリシーの中身を見る
- Authentication → Policies を開く
- 各テーブルのポリシーを1つずつ見る
- 条件(USING / WITH CHECK)に
trueだけが書かれているものを探す
true だけのポリシーがあれば、それは制限していません。
手順3: 書き込みのポリシーがあるか数える
SELECT のポリシーはあるのに INSERT / UPDATE のポリシーが無い、という状態を探します。無い場合、その操作は拒否されているか(正常)、別のポリシーで緩く許可されているか(危険)のどちらかです。
7. AIに貼る修正指示
ここまでの確認をAIに任せる場合は、これをそのまま貼ってください。
Supabase のマイグレーション/SQL を全部読んで、次を表にして報告してください。
(1) RLS が有効になっていないテーブル
(2) RLS は有効だがポリシーが1つも無いテーブル
(3) ポリシーの条件が USING (true) など実質的に全員を許可しているテーブル
(4) SELECT のポリシーはあるが INSERT / UPDATE の WITH CHECK が無いテーブル
そのうえで、各テーブルについて「ログインした本人の行だけ」に絞る
RLS ポリシーの SQL を書いてください。次の条件を守ってください。
- 性能のため auth.uid() は (select auth.uid()) の形で書く
- INSERT と UPDATE には WITH CHECK を必ず付ける
- UPDATE には USING と WITH CHECK の両方を付け、その理由を SQL の
コメントとして書く
- service_role キーを使って RLS を迂回している箇所があれば、
それも一覧にする
私は SQL が読めないので、各ポリシーが「誰に何を許可しているか」を
日本語で1行ずつ説明してください。
書いたポリシーが本当に絞れているかは、無料のRLSチェッカーに貼り付けると判定できます。ブラウザ内で完結するので、SQLがどこかに送信されることはありません。
8. よくある勘違い
「テーブルを非公開にすればRLSは要らない」 Supabase の API は、公開スキーマのテーブルを自動的に公開します。「非公開にする」設定は別途必要で、かつアプリから使うテーブルは結局公開する必要があります。RLSの代わりにはなりません。
「service_role キーを使えば楽」 確かに動きます。RLSを全部素通りするからです。しかしそのキーがブラウザ側に出た瞬間、全データが誰でも読み書きできます。AI が生成したコードでこのキーが使われがちなのは、まさに「エラーが出ないから」です。楽な道が最も危険、という典型例です。
「自分しか使わないアプリだから大丈夫」 本当に自分しか使わず、URLも公開していないなら、リスクは限定的です。ただし「あとで人に見せるかも」と思っているなら、そのときに設定する保証はありません。今やるほうが安上がりです。
「ユーザーが増えてから考える」
RLSはあとから足せますが、すでに漏れたデータは戻りません。そしてRLSを後付けすると、既存のクエリがまとめて動かなくなるので、結局その場で USING (true) を書く羽目になりがちです。
まとめ
- RLS は「データベース側で、誰がどの行を見られるかを決める設定」
- Supabase ではブラウザから直接データベースに問い合わせが飛ぶので、ログイン画面は防御にならない。
anonキーが公開されているのは正常で、RLSが無いことのほうが異常 - 有効にするだけでは足りない。
USING (true)は無効と結果が同じ - 読み取り(
USING)だけでなく書き込み(WITH CHECK)も必要 - 責任の所在は、ツールベンダー自身が「利用者にある」と明言している
RLS は、AIで作ったアプリで最も事故が多い場所です。逆に言えば、ここさえ押さえれば最大のリスクは消えます。設定自体は難しくありません——忘れられるだけです。
まだ全体像を掴めていない方は、AIで作ったアプリを公開する前に — 非エンジニアのための本番リリース全ガイドから読むと、他に何を確認すべきかも分かります。