Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RLSは満点だった。 勝手口は関数にあった。

Avatar for いちご いちご
August 05, 2026
39

RLSは満点だった。 勝手口は関数にあった。

Avatar for いちご

いちご

August 05, 2026

Transcript

  1. 半年前の俺が作っていたもの Next.js 15 + Supabase の資産管理アプリ。口座残高・定期収支・貯蓄予測を管理 開発期間 2025-02 〜 2026-02

    / 369 コミット DB テーブル 8本 / migrations 11本 / Postgres関数 7本 開発スタイル Cursor・Claude Codeに実装をほぼ委任 自分でSQLを書いた回数 ほぼ 0 最終コミット 2026-02-14 — 以後、半年ノータッチ 合同会社ソネット | Supabase Meetup Tokyo #1 8
  2. 「DBが直接開いてる」って、誰でも叩けるのでは? → 叩けます。 DBに繋ぐためのキー(publishable key / 旧 anon key )は、アプリのJSに埋め込んで配

    るもの。 ブラウザのDevToolsを開けば誰でも読めます(事故ではなく仕様) # つまり、アプリの画面を一切経由せずに、手元からこう叩ける curl "https://<project>.supabase.co/rest/v1/accounts?select=*" \ -H "apikey: sb_publishable_..." # ← JSから拾ったキー ログイン画面もボタンの出し分けも全部素通り。守れるのはDBの中だけ 合同会社ソネット | Supabase Meetup Tokyo #1 12
  3. 鍵をかけ忘れた実例 CVE-2025-48757(2025-05公表) AIコーディングツール(Lovable)で生成さ れたアプリでRLSが張られておらず、未 ログインのままメール・決済情報まで読 めた スキャンした 1,645プロジェクト中 170件 (約10%)

    ・303エンドポイント 怖いのは、アプリは正常に動いて見える こと。UI経由では自分のデータしか出な いので誰も気づかない 合同会社ソネット | Supabase Meetup Tokyo #1 ※ Supabase本体の脆弱性ではない(発見: 13
  4. RLSの実体: ただのSQL 2行 -- ① テーブルの鍵を有効化(以後、ポリシーがなければ全部拒否) ALTER TABLE accounts ENABLE

    ROW LEVEL SECURITY; -- ② 「自分の行だけ」を許可するポリシー CREATE POLICY "Users can only access their own accounts" ON accounts FOR ALL USING (auth.uid() = user_id); auth.uid() = 今アクセスしてきた人のユーザーID(Supabaseが提供) 有効化するとデフォルト拒否。ポリシーが唯一の許可リストになる 現在の公式推奨は USING ((select auth.uid()) = user_id) + user_id にイ ンデックス(大規模テーブルで劇的に速くなる) 合同会社ソネット | Supabase Meetup Tokyo #1 14
  5. どう監査するか: DBに聞けばいい SQLファイルを目で読む → 筋が悪い。11ファイルあり、後から ALTER で上書き もある = 最終形が分からない

    適用後のDBに聞く → 正しい。Postgresは今の設定を自分でテーブルに持ってい る SELECT tablename, rowsecurity FROM pg_tables; -- RLS入ってる? SELECT tablename, policyname, qual FROM pg_policies; -- ポリシーの中身は? SELECT proname, prosecdef FROM pg_proc; -- RLSを迂回する関数は? この3クエリで「AIが書いたつもり」ではなく「実際にDBがどうなっているか」が出 る 合同会社ソネット | Supabase Meetup Tokyo #1 16
  6. 本番は触りたくない → ゼロから再現して聞く 本番DBに一切触らない・壊す心配ゼロ migrations 11本 ↓ 適用 素のPostgres 16

    (+ authシム3行) ↓ 同じスキーマ ↓ pg_* に問い合わせ Supabase固有の部分は auth.users と auth.uid() のシム3行で足りた 半年放置した migrations 11本、0.6秒で全 部通った これができたのは migrations が残ってい たから = 半年前の自分に感謝するポイント 合同会社ソネット | Supabase Meetup Tokyo #1 17
  7. 結果①: テーブル側は満点だった 監査項目 結果 RLS有効化 8 / 8 テーブル全部 rowsecurity

    = t ポリシー 11本、全テーブルに auth.uid() の所有チェック 子テーブル 親テーブル経由の EXISTS で所有確認(正しい) INSERT系 WITH CHECK も正しく設定 SQLをほぼ書いていない俺のDBが、教科書どおりだった 合同会社ソネット | Supabase Meetup Tokyo #1 19
  8. 結果②: 関数に勝手口があった 振替用のPostgres関数 7本すべてが SECURITY DEFINER (=RLSを迂回する実行モー ド) CREATE FUNCTION

    delete_one_time_transfer_pair(p_transaction_id UUID) SECURITY DEFINER -- RLSが効かない AS $$ BEGIN SELECT * INTO v_tx FROM one_time_transactions WHERE id = p_transaction_id; -- ↑ 所有者チェックが 1行もない DELETE FROM one_time_transactions WHERE transfer_pair_id = ...; 関数は PostgRESTのRPCとして外からも直接呼べる(アプリ画面は素通り) 読まれる穴ではなく、書かれる・消される穴。UUIDを知れば他人の取引を消せる アプリ経由の正規ルートは安全。だがRLSの境界は関数の外で破れている 合同会社ソネット | Supabase Meetup Tokyo #1 20
  9. じゃあ、これもCVE級? → No 同じ「ターミナルから直接叩く」攻撃でも、RLS漏れと勝手口は危険度が段違い RLS設定漏れ(CVEの型) 勝手口(今回) ログイン 不要(未ログインでOK) 必要(自分の分) 標的のID

    不要 — select * で全部 必要(取引UUID) できること 全ユーザーを読み放題(流出) 書く・消す(1件ずつ) RLSが満点だったから、最悪("全ダンプ")は起きない。 残ったのは限定的な勝手口だ け 合同会社ソネット | Supabase Meetup Tokyo #1 21
  10. 種明かし: ガードレールは入れてあった .cursor/rules/ に Supabase公式のAIルール集(2025-03に導入) 公式ルールの指示 実際のAIの挙動 ポリシーは auth.uid() で所有チェック

    守った(全テーブル) FOR ALL 禁止・4操作に分割 半分無視(守ったのは1テーブル) 関数は SECURITY INVOKER をデフォルトに 無視(7本全部DEFINER) search_path = '' を必ず設定 無視(0本) 合同会社ソネット | Supabase Meetup Tokyo #1 23
  11. 持ち帰り: 「入れて、それでも疑え」 1. ガードレールは入れる。ただし今は配布形態が違う .cursor/rules の .mdc ではなく → 公式Plugin

    / Agent Skills / MCP(AI Tools)に再編 2. 自作の監査より先に、Security Advisor を見る 公式リンター Splinter 内蔵。RLS未有効テーブルは critical、 SECURITY DEFINER も検 出対象 3. それでも疑うなら自前で — migrationsさえあれば素のPostgresで再現して pg_* に聞 ける -- CIに置ける最小の番犬: SECURITY DEFINERの関数が増えたら警報 SELECT proname FROM pg_proc WHERE prosecdef AND pronamespace = 'public'::regnamespace; 合同会社ソネット | Supabase Meetup Tokyo #1 24