メインコンテンツへスキップ
友田 陽大
AIで作ったアプリの運営
バイブコーディング
セキュリティ
Supabase
データベース
個人開発

Supabaseの「RLS」って何? — AIが作ったアプリでデータが全公開になる仕組みを、コードなしで理解する

「ChatGPTにRLSを有効にしろと言われたけど意味が分からない」——そんな方向けに、RLS(行レベルセキュリティ)とは何か、なぜ設定しないと全ユーザーのデータが誰でも読めるのかを、コードを読まずに理解できるよう解説します。確認手順とAIに貼れる修正指示つき。

公開日
読了時間
10分
著者
友田 陽大
シェア

「ChatGPTに聞いたら『RLSを有効にしろ』と言われた。でも意味も、やり方も分からない」

この記事は、その状態から抜け出すためのものです。コードは読めなくて構いません。仕組みが分かれば、あとはAIに指示を出せば直せます。


1. RLS とは何か — 表計算で考える

RLS(Row Level Security、行レベルセキュリティ)は「データベース側で、誰がどの行を見られるかを決める設定」です。

「行」というのは、表計算ソフトの1行だと思ってください。たとえば、あなたのアプリに「メモ」という表があるとします。

iduser_id内容
1Aさん銀行の暗証番号のメモ
2Aさん買い物リスト
3Bさん転職活動のメモ

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: 管理画面で有効/無効を見る

  1. Supabase の管理画面にログイン
  2. 左メニューの Table Editor を開く
  3. テーブル一覧を見る

「RLS disabled」「Unrestricted」といった警告バッジが出ているテーブルがあれば、それは全公開のテーブルです。

手順2: ポリシーの中身を見る

  1. AuthenticationPolicies を開く
  2. 各テーブルのポリシーを1つずつ見る
  3. 条件(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で作ったアプリを公開する前に — 非エンジニアのための本番リリース全ガイドから読むと、他に何を確認すべきかも分かります。

よくある質問

RLS を一言で言うと何ですか?
「データベース側で、誰がどの行を見られるかを決める設定」です。表計算ソフトの1行1行に対して「この行はAさんだけ」「この行はBさんだけ」と鍵をかけるイメージです。アプリのコード側ではなくデータベース側で判定するため、どの経路から問い合わせが来ても同じ制限がかかります。
ログイン画面があるのに、なぜデータが全部見えるのですか?
ログイン画面はブラウザ側の話で、データベースはそれを知らないからです。Supabaseではブラウザからデータベースへ直接問い合わせが飛びます。このとき使う anon キーは公開情報なので、攻撃者はログイン画面を一切通らずに問い合わせできます。ログイン画面は玄関の鍵で、RLSは金庫の鍵です。玄関を通らず窓から入られたら、玄関の鍵は意味がありません。
anon キーがブラウザから見えるのは問題ですか?
問題ありません。anon キーは設計上「公開されるもの」で、ブラウザで読めるのは正常な状態です。危険なのは service_role キーのほうで、これはすべての制限を無視できるため、ブラウザ側に置くと RLS が完全に無意味になります。つまり anon キーが見えること自体ではなく、RLSが無いことが問題です。
RLS を有効にすればもう安全ですか?
有効にしただけでは足りません。RLSは「ポリシー(許可の条件)が1つも無ければ全部拒否」なので有効化した瞬間はアプリが動かなくなり、そこで慌てて USING (true) という条件を書いてしまうケースが非常に多いです。これは「全員が全行を見てよい」という意味で、無効なのと結果は同じになります。条件は auth.uid() = user_id のように本人に絞る必要があります。
読み取りだけ制限すれば十分ですか?
不十分です。読み取りの制限(USING)を書いて書き込みの制限(WITH CHECK)を忘れるのが最も多い抜けで、この状態だと他人のIDを指定したデータを作ったり、自分のデータを他人のものに書き換えたりできてしまいます。UPDATE には USING と WITH CHECK の両方が必要です。前者が「どの行を触れるか」、後者が「どんな値に変えられるか」を決めます。

参考文献

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

「動いている。でも公開して大丈夫か分からない」

20の質問に答えるだけ — AIで作ったアプリの公開前セルフ診断(無料・登録不要)

コードを読む必要はありません。「ログインした人だけが自分のデータを見られるようになっていますか?」のような質問に、はい/いいえ/わからない で答えるだけです。危険な順に並んだ結果と、そのまま Claude Code や Cursor に貼れる修正指示が出ます。入力内容はブラウザの外に出ません。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

あわせて読みたい