読了 約 7 分 Cosoado Lab

Supabase RLS の UPDATE ポリシーで USING を省くと他人データを乗っ取れる

Supabase RLS の UPDATE ポリシーで WITH CHECK だけ書いて USING を省くと、攻撃者が任意の行を UPDATE で自分の所有に転換できてしまう。USING (触れる行) と WITH CHECK (書ける行) の違いを整理し、コマンド別 (SELECT/INSERT/UPDATE/DELETE) の正解パターンと pg_policies での棚卸し SQL までを示す。

TL;DR

役割効くコマンド
USING既存行フィルタ(「触れる行」を決める)SELECT / UPDATE / DELETE
WITH CHECK書き込み後の行バリデーション(「書いた結果」を検査する)INSERT / UPDATE
-- 危険: USING がないと全行が UPDATE の標的になる
create policy "update own profile"
on profiles for update
to authenticated
with check ( (select auth.uid()) = user_id );

-- 正解: UPDATE は USING + WITH CHECK を両方書く
create policy "update own profile"
on profiles for update
to authenticated
using  ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

USING と WITH CHECK、何が違うのか

Supabase の公式ドキュメント(Row Level Security)には次の一文があります。

If no with check expression is defined, then the using expression will be used both to determine which rows are visible and which new rows will be allowed to be added.

PostgreSQL の仕様(CREATE POLICY)でも同様に、コマンドごとに適用される句が決まっています。

UPDATE は既存行と新規行の両方に関わる唯一のコマンドなので、USING と WITH CHECK のどちらが欠けてもロジックが崩れる

危険パターン①: UPDATE ポリシーに USING がない

これが一番やった失敗です。「更新の中身さえチェックすればいい」と思って WITH CHECK だけ書きました。

-- これは穴がある
create policy "users can update own profile"
on profiles for update
to authenticated
with check ( (select auth.uid()) = user_id );

USING 句がないので、このポリシーはどの行でも UPDATE の標的にできます。攻撃者が踏む手順はこうです。

  1. User A(uid = 'aaaa-...')が User B の行(id = 42, user_id = 'bbbb-...')を標的にする
  2. UPDATE profiles SET user_id = 'aaaa-...', display_name = 'hijacked' WHERE id = 42 を実行
  3. USING チェック: 定義なし → スルー
  4. WITH CHECK: (select auth.uid()) = user_id'aaaa-...' = 'aaaa-...' → 通過
  5. 結果: User B の行が User A の所有に書き換わる

WITH CHECK が検査するのは「書いた後の状態」だけです。「誰が更新できるか」は USING が担います。USING なしでは誰でも任意の行を自分のものに転換できます。

危険パターン②: FOR ALL に WITH CHECK がない(暗黙フォールバック)

Supabase の Dashboard が自動生成するポリシーで見かけるパターンです。

-- 動くが意図が不明確
create policy "users own data"
on profiles for all
using ( (select auth.uid()) = user_id );
-- WITH CHECK 省略 → PostgreSQL が USING 式を WITH CHECK に流用

WITH CHECK を省略すると PostgreSQL は USING 式を WITH CHECK としても使います。INSERT で user_id ≠ auth.uid() な行も弾いてくれるので、動作上は意図通りになることが多い。

ただし暗黙のフォールバックに頼ると、後でポリシーを修正したとき「なぜ WITH CHECK が書いていないのか」で混乱します。私は意図を明示するため FOR ALL でも両方書くようにしました。

-- 意図を明示する
create policy "users own data"
on profiles for all
using  ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

コマンド別の正しい書き方

-- SELECT: USING のみ
create policy "read own profile"
on profiles for select
to authenticated
using ( (select auth.uid()) = user_id );

-- INSERT: WITH CHECK のみ
create policy "insert own profile"
on profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );

-- UPDATE: USING + WITH CHECK 両方
create policy "update own profile"
on profiles for update
to authenticated
using  ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

-- DELETE: USING のみ
create policy "delete own profile"
on profiles for delete
to authenticated
using ( (select auth.uid()) = user_id );

Supabase 公式は「UPDATE 操作を行うには、対応する SELECT ポリシーが必要」とも注記しています。SELECT ポリシーがないと UPDATE が期待通り動かないことがあるので、UPDATE と SELECT はセットで定義します。

なお (select auth.uid())() で包んでいるのは別記事で書いた 1 行最適化です(詳しくは Supabase RLS で auth.uid() を毎行呼び出さないための 1 行の書き換え)。

現行ポリシーの棚卸し

pg_policies ビューで USING / WITH CHECK の状態を確認できます。

select
  tablename,
  policyname,
  cmd,
  qual       as using_expr,
  with_check as with_check_expr
from pg_policies
where schemaname = 'public'
order by tablename, cmd;

cmd = 'UPDATE' の行で using_expr が NULL なら危険パターン①に該当します。先に気づくべきでしたが、SparMate の profiles / matches テーブルを棚卸ししたとき、UPDATE ポリシーのうち using_expr が NULL のものが 2 つありました。気づかず本番に出ていたと思うと冷や汗でした。

修正は drop してから create が基本です(CREATE OR REPLACE POLICY は使えません)。

drop policy if exists "users can update own profile" on profiles;

create policy "users can update own profile"
on profiles for update
to authenticated
using  ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

まとめ

関連プロダクト

他のプロダクト by Cosoado Lab

TsuriMate 釣り仲間・船割りメンバーマッチング YUMELIA 夢占い・数秘術が無料でできる AI 占い Web アプリ OshiVista 推し活を 1 つにまとめる管理アプリ(多言語対応) 謎かけメーカー AI と「その心は?」を競うなぞかけ・大喜利