Claude Media
Claude CodeでSupabaseのRLSポリシーの抜けを洗い出すレビュー用サブエージェント

Claude CodeでSupabaseのRLSポリシーの抜けを洗い出すレビュー用サブエージェント

RLSを有効にし忘れたテーブルと、using句がtrueのポリシーをmigrationのSQLから洗い出す、読み取り専用のサブエージェントの作り方。定義ファイル、事前チェックのスクリプト、直し方までを載せます。

SupabaseのRLS(Row Level Security、行レベルセキュリティ)は、テーブルを増やすたびに有効化とポリシーの追加が要る仕組みです。1つ忘れると、そのテーブルは権限のある誰でも読み書きできる状態で公開されます。この点検を人の目だけに頼ると、migrationが増えるほど見落としが出ます。

Claude Codeのサブエージェントなら、点検の観点を定義ファイルに固定して、supabase/migrations のSQLを毎回同じ基準で読ませられます。ここでは、書き込み権限を持たないレビュー専用のサブエージェントを作り、機械的に拾える抜けはスクリプトで先に拾い、見つかった指摘を直してテストで確かめるところまでを順に追います。

レビューを任せる範囲はmigrationのSQLに絞る

最初に決めるのは、サブエージェントに何を見せるかです。この記事では、リポジトリにある supabase/migrations/*.sql と supabase/tests/ を読んで指摘するだけの構成にします。データベースには接続しません。

この絞り方には理由があります。接続を持たせると、本番データに触れる経路が1本増えます。Supabaseを接続して管理操作まで任せる方法はSupabase MCPサーバーの手順にあります。ポリシーの点検に必要なのはSQLの文面なので、読むだけで足ります。

静的に読む構成が拾えるものと拾えないものは次のとおりです。

見つけられる見つけられない
enable row level security の書き忘れ見つけられないダッシュボードのSQL Editorで直接実行した変更
using ( true ) のポリシー見つけられない実データの中身が想定どおりか
to 句の欠落、for all の使用見つけられない本番プロジェクトで実際に付いているgrantの現状
security definer 関数の search_path 未固定見つけられないmigrationに載っていない拡張やロールの設定

右の列は、データベースの現状を読まないと分かりません。この限界は後半の節で、テストによる確認で補います。

サブエージェントの定義ファイルを置く

サブエージェントは、YAMLのfrontmatterとMarkdownの本文でできたファイルです。name と description の2つだけが必須で、本文がサブエージェントのシステムプロンプトになります。プロジェクト用は .claude/agents/ に置き、バージョン管理に入れればチームで共有できます。

レビュー専用にするため、tools に Read, Grep, Glob だけを書きます。tools は許可リストとして働くので、ファイルの編集や書き込み、MCPツールは使えません。Bashも渡さないので、psql を叩く経路もありません。

mkdir -p .claude/agents

.claude/agents/supabase-rls-reviewer.md を次の内容で作ります。点検項目は、Supabaseの手順にある「テーブルごとにやること」と注意書きから引いています。

---
name: supabase-rls-reviewer
description: Reviews Supabase SQL migrations for missing or weak Row Level Security. Use proactively after adding or changing tables, policies, views, or functions under supabase/migrations.
tools: Read, Grep, Glob
model: inherit
---
 
あなたは Supabase の RLS を点検するレビュー担当です。
ファイルは編集せず、SQL も実行しません。読んで指摘するだけです。
 
手順:
1. supabase/migrations/*.sql をファイル名の昇順で全部読む。
   後のファイルが前のテーブルやポリシーを変更・削除していることがあるので、
   最終的に残る状態を追う。
2. supabase/tests/ に、テーブルごとの RLS テストがあるか確認する。
3. 下の点検項目を、テーブル・ビュー・関数ごとに当てる。
 
点検項目:
- create table した public スキーマのテーブルに enable row level security が無い
- revoke all ... from anon, authenticated が無い(policy だけでは grant は消えない)
- using ( true ) または with check ( true ) のポリシー。
  anon が対象に入っていれば、未ログインの訪問者に全行を公開する意味になる
- ポリシーに to 句が無い
- for all のポリシー(操作ごとに分ける)
- auth.uid() を select で包まず直接呼んでいる、または null を考慮していない
- auth.jwt() の user_metadata をアクセス判定に使っている
  (利用者本人が更新できる。判定には app_metadata を使う)
- update のポリシーに with check が無い、対応する select ポリシーが無い
- security definer の関数に set search_path = '' が無い、または公開スキーマにある
- ビューが security_invoker = true になっていない
- 2 つのテーブルのポリシーが互いを参照している(再帰)
 
出力は重大度順の表にする。列は「ファイル:行」「テーブル」「問題」「直し方の方針」。
該当が無い項目は書かない。確信が持てないものは「要確認」と明記する。

description には「Use proactively」を入れています。自動委譲の判断は description の文面に基づくので、こう書くとテーブルやポリシーに触れたあとにClaudeが呼びやすくなります。ただし、確実に動かしたい場面では後述の呼び出し方で名前を指定します。

定義ファイルの変更は、数秒以内に検出されて次の委譲から使われます。ただし、.claude/agents/ を新しく作った直後のセッションでは再起動が要ります。上のコマンドでディレクトリを先に作った場合は、そのあとで起動し直してください。

機械で拾える抜けはスクリプトで先に拾う

「RLSが有効でないテーブル」は、言語モデルに読ませなくても、create table と enable row level security の差分で出せます。決まった答えが出る点検は、スクリプトで先に済ませたほうが速く、結果もぶれません。サブエージェントには、スクリプトでは判断できない部分に集中してもらいます。

次のスクリプトは、migrationに書かれたテーブル名と、RLS有効化が書かれたテーブル名を突き合わせます。あわせて using ( true ) と with check ( true ) の行も出します。

#!/bin/bash
# scripts/rls-precheck.sh
# RLS の機械的な事前チェック。migrations の SQL だけを見る。
DIR="${1:-supabase/migrations}"
created=$(grep -rhoiE 'create table (if not exists )?(public\.)?[a-z_]+' "$DIR" \
  | awk '{print tolower($NF)}' | sed 's/^public\.//' | sort -u)
enabled=$(grep -rhoiE 'alter table (public\.)?[a-z_]+ enable row level security' "$DIR" \
  | awk '{print tolower($3)}' | sed 's/^public\.//' | sort -u)
echo "== RLS が有効化されていないテーブル =="
comm -23 <(echo "$created") <(echo "$enabled")
echo "== using (true) / with check (true) の行 =="
grep -rnEi '(using|with check) *\( *true *\)' "$DIR"

動かすために、意図的に抜けを入れた小さなmigrationを用意しました。profiles は有効化とポリシーが揃い、notes は有効化が無く、announcements は using ( true ) と、to 句の無い with check (true) のINSERTポリシーを持ちます。

chmod +x scripts/rls-precheck.sh
./scripts/rls-precheck.sh

このサンプルに対する出力は次のとおりです(bash 3.2、macOSで実行)。

== RLS が有効化されていないテーブル ==
notes
== using (true) / with check (true) の行 ==
supabase/migrations/20261001000000_init.sql:24:using ( true );
supabase/migrations/20261001000000_init.sql:26:with check (true);

notes は有効化の書き忘れとして出ました。24行目の using ( true ) は、announcements の読み取りを anon にも開くSELECTポリシーです。お知らせの公開テーブルなら意図どおりかもしれません。26行目のINSERTは to 句が無く、with check (true) で誰の書き込みも通します。こちらは、書き込み側のgrantを外していなければ危険な形です。

このスクリプトには限界があります。using ( と true が別の行に分かれて書かれていると拾えません。alter table が if exists を挟む書き方や、複数のmigrationをまたいだ drop policy も追いません。「疑わしい行の候補を出す道具」と割り切り、最終判断は次のサブエージェントに渡します。

点検項目はSupabaseの手順から引く

定義ファイルに並べた項目には、それぞれ根拠があります。見逃しやすいものを、Supabaseの手順の記述と合わせて表にします。

項目危ない形理由
RLSの有効化漏れ危ない形create table だけで終わる理由公開スキーマのテーブルは、grantを持つ役割なら誰でも読み書きできる
grantの取り残し危ない形policyだけ書いて revoke しない理由既存プロジェクトでは新しいテーブルに全権限が付き、policyを足しても消えない
anon への全開放危ない形to anon using ( true )理由未ログインの訪問者全員に、grantが届く範囲の全行を読ませる
user_metadata の利用危ない形auth.jwt() の user_metadata で権限を判定理由認証済みの利用者本人が書き換えられる
with check の欠落危ない形updateに using だけを書く理由更新後の行がどうなってよいかを縛れず、他人の行へ付け替えられる
ビュー危ない形通常のビューで保護済みテーブルを公開理由ビューは既定でRLSを素通りする

「grantの取り残し」は、公式の手順が明示している点です。新しいテーブルの public には、anon、authenticated、service_role の3つの役割へ全権限が付く構成のプロジェクトがあります。ポリシーを足してもそのgrantは取り上げられません。一方で、手順には「すべてのプロジェクトが自動で付与するわけではない」とも書かれています。自分のプロジェクトがどちらかは、レビューの前に確認してください。サブエージェントにも、判断材料としてこの違いを伝えておく価値があります。

auth.uid() の扱いにも落とし穴があります。未認証のリクエストでは null を返すため、auth.uid() = user_id は常に偽になります。静かに通らないだけで、エラーにはなりません。Supabaseは auth.uid() IS NOT NULL AND auth.uid() = user_id と書いて意図を明示するよう勧めています。また、(select auth.uid()) のように select で包むと、Postgresのオプティマイザが結果を文ごとに使い回せます。ただし、行の値に応じて結果が変わる式には使えません。

呼び出し方は3通りある

サブエージェントの呼び出しは、強さの違う3つの方法があります。

  • 自然言語で名前を出す。Claudeが委譲するか判断します
  • @ でメンションして選ぶ。そのサブエージェントが必ず動きます
  • --agent フラグか agent 設定で、セッション全体をそのサブエージェントとして動かす

レビューの用途では、2番目が使いやすい形です。次のように書きます。

@"supabase-rls-reviewer (agent)" 今日追加した migration の RLS を点検して

メンションで決まるのは、どのサブエージェントを呼ぶかだけです。サブエージェントに渡す依頼文は、Claudeがユーザーの発言をもとに組み立てます。点検の観点は定義ファイル側に固定してあるので、依頼文がぶれても基準は変わりません。

3番目の --agent は、セッション全体にレビュー役のツール制限とモデルを適用します。CIのように、レビューだけを走らせる使い方に向きます。

claude --agent supabase-rls-reviewer

指摘が出たら、直してテストで確かめる

レビュー担当は読み取り専用なので、直すのはメインの会話の仕事です。サブエージェントの表を貼り付けて、修正を依頼します。

たとえば、先ほどのサンプルの notes に対する直しは、Supabaseの手順にある並びそのままです。有効化、grantの取り上げ、必要な権限だけの付与、操作ごとのポリシーの順に書きます。以下は、公式の例に沿った形です。

alter table public.notes enable row level security;
revoke all on table public.notes from anon, authenticated;
grant select, insert, update, delete on table public.notes to authenticated;
 
create policy "Users can view their own notes."
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );
 
create policy "Users can create their own notes."
on public.notes for insert
to authenticated
with check ( (select auth.uid()) = user_id );
 
create policy "Users can update their own notes."
on public.notes for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
 
create policy "Users can delete their own notes."
on public.notes for delete
to authenticated
using ( (select auth.uid()) = user_id );

公式の手順は、操作ごとにポリシーを分けるよう求めています。Postgresは1つの for 句に複数の操作を書けず、for all にすると各ルールがどの操作を想定したのか分からなくなるためです。UPDATEを通すには対応するSELECTポリシーも要るので、上の4つは揃えて入れます。

前と後

announcementsのINSERTポリシー

BEFORE

to句が無く、全員を通す

create policy "anyone writes" on public.announcements for insert
with check (true);
AFTER

書き込み権を持つ役割だけに絞る

-- 読み取り専用のフィードなら insert の grant と policy をそもそも持たせない
revoke insert, update, delete on table public.announcements from anon, authenticated;
drop policy "anyone writes" on public.announcements;

Supabaseの手順でも、クライアントが読むだけで書かないテーブルには select だけを付与します。書き込みは、バックエンドのジョブが service_role で行う構成です。この役割はRLSを迂回するので、ブラウザには出さずサーバー側に置きます。シークレットキーが使われていても、リクエストにユーザーのアクセストークンが付いていれば、そのユーザーのRLSの下で動く点も知っておくと混乱が減ります。

直したら、テーブルごとのテストを書いて走らせます。公式の手順は、supabase/tests/ にallowとdenyを検証する .sql を置き、supabase test db で実行する流れを示しています。

supabase test new notes_rls.test
supabase test db

テストの書き方では、1つ守る点があります。許可されるはずの書き込みを lives_ok で確かめないことです。ポリシーが行を0件に絞った場合でも lives_ok は通ります。returning を付けて、返った値を検証します。拒否の確認も、行が0件だったことだけで終わらせず、対象の行が無傷であることを読み返して組にします。

定義ファイルの手順2は、この観点を拾うためのものです。該当テーブルのテストが無ければ、指摘に含まれます。ポリシーの文面が正しく見えても、テストが無ければ意図どおりに動く保証はありません。

静的レビューで届かない範囲をどう補うか

migrationだけを読む構成には、限界があります。補う手段を3つ挙げます。

  1. ダッシュボードで直接変更する運用をやめる。変更はすべてmigrationに残し、レビューの対象に入れる
  2. ビューと関数はサブエージェントに必ず見せる。保護済みテーブルの上に作った通常のビューは行を素通りさせ、security definer の関数は作成者の権限で動きます
  3. 点検の最後に、supabase test db の結果をレビューの入力にする。ポリシーが想定どおりに動いたかは、実行した結果でしか分かりません

2つ目の関数については、公式の手順に守るべき条件が2つあります。set search_path = '' を付けて関数内の名前をスキーマ付きで書くこと。そして、APIの「Exposed schemas」に含まれるスキーマには作らないことです。公開スキーマの security definer 関数は、Data API経由で作成者の権限のまま呼べてしまいます。定義ファイルの項目に入れたのは、この理由からです。

サブエージェントの権限をもっと厳密に縛りたい場合は、権限ルールの設計が参考になります。DBに接続するサブエージェントなら、書き込み系のSQLをhookで止める読み取り専用DBの構成が使えます。同じ差分を別の文脈に見直させる方法は、敵対的レビューの設計にまとまっています。サブエージェント全般の作り方はSubagents完全ガイドの範囲です。

まとめ

RLSの抜けは、機械で出せるものと、読んで判断するものに分けると扱いやすくなります。有効化の書き忘れや using ( true ) の行はスクリプトで候補を出し、to 句、with check、security definer といった文脈が要る点検は、読み取り専用のサブエージェントに任せます。直した結果は supabase test db で確かめ、その結果を次のレビューに戻します。

書き込み権限を持たせないという選択も、この構成の中心です。点検役がSQLを書き換えないので、指摘を採るか捨てるかは人が決められます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/sub-agents
  2. supabase.com/docs/guides/database/postgres/row-level-security
この記事を共有:XはてブLinkedIn