Insights·2026-08-14

Supabase RLS — あなたのアプリのデータベースは今、誰にでも開いていないか

Supabase で作ったアプリは、キーがブラウザに出ているのが正常です。そのキーが安全かどうかは、テーブルごとに RLS(行レベルセキュリティ)が有効かどうか、その一点で決まります。RLS が無効なら、そのキーを持つ誰もがデータベースを読み書きできます。セキュリティ企業 Wiz が 2026 年 2 月に公開した Moltbook の事例がまさにそれで、ブラウザの JavaScript に置かれた publishable キー 1 本で約 475 万件のレコードが開いていました。確認は 3 分です。公開中のサイトでキーを探し、Supabase ダッシュボードの Table Editor で Unrestricted バッジを見る。本記事ではその手順と、RLS を有効にした直後に画面が空になる理由、ポリシーを足す順序を説明します。

Supabase RLS 요약 도식. 위쪽은 「RLS 꺼짐」과 「RLS 켜짐」을 좌우로 나란히 놓았다. 꺼진 쪽에는 publishable 키를 가진 누구나 데이터베이스를 읽고 쓸 수 있다는 설명과, Moltbook이 브라우저 키 하나로 약 475만 건의 레코드를 열어 두고 있었다는 사례가 적혀 있다. 켜진 쪽에는 alter table … enable row level security 명령과 함께, 정책이 허용한 것만 나가며 정책을 하나도 만들지 않으면 아무 데이터에도 접근되지 않는다는 설명이 붙어 있다. 가운데는 점검 3단계다 — 배포된 사이트에서 F12 → Sources → supabase로 키 찾기, Table Editor에서 Unrestricted 배지 찾기, 켜고 정책 붙이기. 아래는 Supabase SQL Editor 화면으로, 테이블마다 한 줄인 alter table profiles enable row level security와, 지금 열려 있는 테이블을 rowsecurity가 false인 줄로 찾는 select tablename, rowsecurity from pg_tables where schemaname = 'public' order by rowsecurity 질의가 찍혀 있다.
왼쪽과 오른쪽을 가르는 것은 키가 아니라 RLS 한 줄이다. 켠 직후 화면이 비어 보이는 것은 고장이 아니라 정책이 아직 없는 것이다.

RLS とは何か、公開キーがなぜそれ一つに懸かるのか

Supabase はブラウザがデータベースに直接話しかける構造です。だからアプリを作るとキーが 2 本出てきます。1 本は publishable キー(旧称 anon)、もう 1 本は secret キー(旧称 service_role)です。前者はブラウザに載って出ていくのが正常で、後者は決してブラウザに置いてはいけません。

Supabase の公式ドキュメントは、publishable キーについてウェブページ・モバイルアプリ・デスクトップアプリ・ソースコードに露出しても安全だと明記しています。ただしその安全は条件付きです。キー自体には「誰が何を見てよいか」が入っていないからです。その判断を代わりに行うのが RLS です。

RLS は Row Level Security、行レベルセキュリティです。テーブルの 1 行ごとに「この要求者はこの行を読んでよいか」をデータベース自身に判定させる機能です。もともと Postgres にある機能で、Supabase はそれを有効にしてルールを書く画面を提供しているだけです。

つまり流れはこうなります。キーは公開される。誰でもデータベースに話しかけられる。RLS が有効でポリシーがあれば、ポリシーが許したものだけが出ていく。RLS が無効なら全部出ていく。その間に段はありません。

キーの種類新しい名称旧名称ブラウザでの扱いRLS の適用
公開用sb_publishable_...anon正常(露出が前提)受ける
非公開用sb_secret_...service_role禁止回避する(BYPASSRLS)

Moltbook はどう開いていたのか

Moltbook 事故の要約図。見出しは「権限エラーなしにデータがそのまま返ってきた」で、公開キーで開いていたものを 4 行に並べる — API 認証トークン150万件はキー一つでそのまま取得でき、オーナーのメール3万5千件と事前登録メール29,631件が一緒にあり、エージェント同士がやり取りした非公開メッセージも開いていた。最後の行は「通報 21:48 → 修正 01:00」で、Wiz 公開タイムラインの UTC 基準と書かれている。

Moltbook は AI エージェント同士が投稿し合うソーシャルサービスで、公開直後から急速に広まりました。創業者は X に「コードは 1 行も書いていない。技術構成の構想があっただけで、それを現実にしたのは AI だ」と書いています。

セキュリティ企業 Wiz が、配信されている JavaScript ファイルの中に Supabase のアドレスと publishable キーを見つけました。ここまでは事故ではありません。前述のとおり、そのキーは見えているのが正常です。

問題はその次でした。研究者がそのキーでデータベースに直接リクエストを送ると、権限エラーもなくデータがそのまま返ってきました。RLS が無効だったのです。スキーマをたどると約 475 万件のレコードが開いており、その中に API 認証トークン 150 万件、所有者のメールアドレス 3 万 5 千件、事前登録者のメールアドレス 29,631 件、エージェント同士の非公開メッセージが含まれていました。

Wiz はこう書いています。行レベルセキュリティを正しく設定していれば公開 API キーは露出しても安全だが、RLS ポリシーがなければそのキーは持っている誰にでもデータベース全体へのアクセス権を与えてしまう。Moltbook の実装にはこの決定的な防御線が欠けていた。Wiz が公開したタイムラインでは、1 月 31 日 21:48 UTC に最初の通報、2 月 1 日 01:00 UTC に全テーブルの対応が終わり修正完了となっています。

この件から持ち帰るべきなのは「バイブコーディングは危険だ」ではありません。AI は動くコードを作ることに最適化されており、アクセス権の設計まで代わりに判断してはくれない、ということです。Wiz も同じ趣旨を書いています。

3 分点検その 1 — 公開中の自分のサイトでキーを探す

まず、自分のアプリが実際にブラウザへ何を送り出しているかを見ます。開発中の画面ではなく、インターネットに上がっているアドレスを開きます。

Chrome でそのページを開き、F12(Mac は Command+Option+I)で開発者ツールを開きます。Sources タブに移り、Command+F または Ctrl+F で全体検索を開いて supabase と入力します。Network タブで再読み込みしてから検索しても同じものが見えます。

ここでプロジェクトのアドレスと、sb_publishable_ または eyJ で始まる長い文字列が見えれば正常です。繰り返しますが、これが見えること自体は事故ではありません。ブラウザからデータベースを呼ぶ構造では隠しようがないからです。

事故なのは、sb_secret_ で始まる値や service_role という語が見える場合です。このキーは RLS をまるごと回避するので、ポリシーをどれだけ丁寧に書いても意味がありません。見つけたら Supabase ダッシュボードでそのキーを失効(rotate)させ、それを使っていたコードをサーバー側へ移します。

3 分点検その 2 — Table Editor で Unrestricted を探す

次に本当の防御線を見ます。supabase.com にログインしてプロジェクトを開き、左メニューの Table Editor に入ります。

テーブル一覧では、RLS が無効なテーブルに保護されていない印(Unrestricted バッジ)が付きます。この印が付いたテーブルは、今この瞬間 publishable キーを持つ誰もが読み書きできる状態です。

ダッシュボードの Table Editor で作ったテーブルは RLS が既定で有効です。危ないのは SQL Editor で create table 文を使って作ったテーブルです。AI に「テーブルを作って」と頼むとたいていこの経路になり、そのとき RLS は無効のまま残ります。バイブコーディングで作ったアプリがこの罠に頻繁にかかる理由がここにあります。

テーブルが複数あるなら全部見ます。1 つでも開いていれば、そこに入っているものは公開されているのと同じです。

有効化は 1 行、そして画面が空に見える理由

左メニューの SQL Editor を開き、テーブルごとに 1 行を実行します。profiles のところに自分のテーブル名を入れます。

有効にしてアプリを再読み込みすると、一覧がまったく空に見えることがあります。慌てなくて大丈夫です。故障ではなく、仕様どおりに動いた結果です。Supabase のドキュメントが明記しているとおり、RLS を有効にしてポリシーを 1 つも作らなければ、publishable キーではどのデータにもアクセスできません。

データが消えたわけではありません。データベースにはそのままあり、ただ「この要求者に見せてよい」と言ってくれるポリシーがまだ無いだけです。次の節でそのポリシーを足します。

順序を逆にしないでください。ポリシーを先に作って RLS を後から有効にするのではなく、有効にして全部閉じたうえで必要なものだけ開ける。この順序なら、見落としたテーブルが開いたまま残りません。

SQL Editor
alter table profiles enable row level security;

ポリシーを書く — まず読み取りから

RLSポリシーSQLの2例—誰でも読める表は to anon と using ( true )、本人の行だけ見る表は auth.uid() 条件を使う。

ポリシーは「どの操作を(select・insert・update・delete)、誰に(anon・authenticated)、どんな条件で許すか」を書く一文です。名前は後で見て分かるように付けます。

お知らせのようにログインなしで誰が見てもよいテーブルなら、こう書きます。to anon はログインしていない訪問者を指し、using ( true ) は条件なしに全部許すという意味です。

ログインした人が自分の分だけを見るテーブルなら条件が変わります。auth.uid() は今ログインしている利用者の識別子で、それが行の user_id と一致するときだけその行を返します。

書き込みは読み取りとは別に許可する必要があります。select ポリシーだけを付ければ読み取りだけが開き、insert・update・delete は閉じたままです。操作ごとに別々のポリシーを書くのが正しく、既定が閉じていることが安全の要です。

よくある失敗が 1 つ。画面が空なのがもどかしくて、すべてのテーブルに using ( true ) を貼ってしまうことです。それでは RLS を有効にする前とまったく同じ状態に戻ります。目標は画面が動くことではなく、出てよいものだけが出ることです。

誰が読んでもよいテーブル
create policy anyone_can_read
on notices for select
to anon
using ( true );
自分の行だけを見るテーブル
create policy own_rows_only
on profiles for select
using ( (select auth.uid()) = user_id );

AI にどう指示し、今日何を残すか

この点検は AI に任せられます。ただし指示は具体的でなければなりません。「セキュリティを点検して」と言えば一般論が返ってきますし、最悪の場合はすべてを開けてしまうポリシーを提案されます。

確認用の SQL も 1 つ覚えておくとよいでしょう。ダッシュボードをテーブルごとにクリックせず、一覧で一度に見られます。rowsecurity が false の行が、今開いているテーブルです。

今日残すものは 3 つです。1 つ、公開中のアプリに secret キーが見えないという確認。2 つ、public スキーマのすべてのテーブルで RLS が有効だという確認。3 つ、テーブルごとになぜそのポリシーなのかを 1 行のメモに残すこと。3 つ目が、次にテーブルを足すときに同じ失敗を防いでくれます。

これはサービスを作る前にやることではなく、すでに上げてあるものに今日やることです。Moltbook は人が集まっている間ずっと開いており、直すのに数時間かかりました。

AI に渡す指示文
私の Supabase プロジェクトの public スキーマにある全テーブルについて、RLS の有効・無効を表にまとめて。
無効なテーブルごとに、(1) 中のデータが公開されてよいかをまず私に尋ね、
(2) 私の答えに合わせて alter table 文と最小権限のポリシー SQL を別々に提案して。
ポリシーは select・insert・update・delete をそれぞれ書き、
using ( true ) は私が公開と答えたテーブルにだけ使って。
RLS の状態を一覧で見る
select tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by rowsecurity, tablename;
出典: Wiz Research