最初に結論を述べます。Supabase(PostgreSQL)の行レベルセキュリティ(RLS)でいちばん事故が多いのは、USING と WITH CHECK の混同です。USING は「どの“既存行”が見えるか」を決める読み取りフィルタ、WITH CHECK は「どの“新しい行”を書いてよいか」を決める書き込みフィルタで、両者はまったく別の関門です。 読み取り側(USING)だけを丁寧に書き、書き込み側(WITH CHECK)を欠いた——あるいは with check (true) で黙らせた——ポリシーは、**認証済みの正規ユーザーが「他人の user_id を持つ行」や「他テナントの id を差し込んだ行」を作れてしまう『書き込みバイパス』**を開きます。
これは直感に反するねじれた事故です。「自分の行しか読めない」テーブルに、「他人の行を書ける」という穴が同居する。読み取りのテストは通り、デモでも顕在化せず、本番で初めて——あるいは攻撃者にだけ——露見します。本記事は、USING と WITH CHECK の違いをコマンド別に正確に押さえ、INSERT/UPDATE/ALL の脆弱SQLを実際の攻撃とともに分解し、修正SQLと検出・検証の手順まで、PostgreSQL/Supabase の一次情報に基づいて解説します。これは Next.js × Supabase アプリケーションセキュリティ完全ガイド で「垂直リスク(設計でしか守れない認可)」と呼んだ層を、書き込み側に絞って深掘りするものです。
1. 結論を分解する:USING と WITH CHECK は別の関門
RLS のポリシーには、条件式を書く場所が2つあります。USING と WITH CHECK です。役割は明確に分かれています(PostgreSQL: CREATE POLICY)。
USING(既存行の検査) — すでにテーブルに存在する行に対して評価され、trueになった行だけが「見える/操作対象にできる」。これは読み取り側のフィルタです。WITH CHECK(新しい行の検査) —INSERTやUPDATEで生まれる新しい行の内容に対して評価され、false(または null)になればエラーで弾かれる。これは書き込み側のフィルタです。公式は明記しています——「check_expressionは行の元の内容ではなく提案された新しい内容に対して評価される」。
つまり USING は「どの行を見せるか」、WITH CHECK は「どんな行を書かせるか」。読み取りを縛っても書き込みは縛られません。 この一点が、本記事のすべてです。
どのコマンドでどちらが効くかを表にします。これは PostgreSQL: CREATE POLICY の「Policies Applied by Command Type」に対応します。
| コマンド | USING(既存行に対する検査) | WITH CHECK(新しい行に対する検査) |
|---|---|---|
| SELECT | 効く(見える行を絞る) | 指定できない |
| INSERT | 指定できない(既存行が無い) | 効く(作る行を検査する) |
| UPDATE | 効く(更新できる既存行を絞る) | 効く(更新後の行を検査する) |
| DELETE | 効く(削除できる既存行を絞る) | 指定できない |
| ALL | 効く | 効く |
ここから読み取れる重要な事実は3つです。
SELECTとDELETEはUSINGだけ。読む・消すは「既存行」の話なので、新しい行の検査(WITH CHECK)は出番がありません。INSERTはWITH CHECKだけ。既存行が無いのでUSINGは指定すらできません(公式:「INSERTポリシーはUSING式を持てない」)。つまり挿入の安全性はWITH CHECKただ一つに懸かっている。UPDATEは両方効く。「どの既存行を更新できるか」をUSINGで、「更新後の行に何を許すか」をWITH CHECKで、二段で縛ります。
RLS そのものの位置づけは Supabase: Row Level Security と PostgreSQL: Row Security Policies が一次情報です。RLS を「有効化した」ことと「読み書きの両方が正しく効いている」ことは別物——本記事はその後半、書き込み側の検証を扱います。
2. UPDATE/ALL のフォールバックという“落とし穴つきの安全網”
ここで、多くの解説が省く重要な挙動を正確に押さえます。UPDATE と ALL のポリシーで WITH CHECK を省略すると、PostgreSQL は USING 式を WITH CHECK にも流用します。 公式の言葉です——「USING と WITH CHECK の両方を持てるポリシー(ALL と UPDATE)で WITH CHECK が定義されていない場合、USING 式が、見える行を決める用途(通常の USING)と、追加を許す新しい行を決める用途(WITH CHECK)の両方に使われる」(PostgreSQL: CREATE POLICY)。
PostgreSQL: Row Security Policies のマネージャー例も同じ趣旨です——「このポリシーは USING と同一の WITH CHECK 句を暗黙に提供するので、制約は選択される行にも変更される行にも適用される(=他のマネージャーの行を INSERT/UPDATE で作れない)」。
これは一見ありがたい安全網ですが、3つの理由で頼ってはいけません。
INSERTにはフォールバックが無い。INSERTポリシーはUSINGを持てないので、流用元がそもそも存在しません。挿入はWITH CHECKを自分で書くしかない。with check (true)を書いた瞬間に消える。 「省略」したときだけ流用されるので、明示的にwith check (true)と書くとフォールバックは働かず、新しい行は無検査になります(後述する最頻出の事故)。USINGがピン留めしていない列は守られない。 フォールバックはUSING式をそのまま再利用するだけ。using (user_id = auth.uid())は新しい行のuser_idは縛りますが、roleやtenant_idのような別の列の書き換えは素通りさせます(第5節で詳述)。
整理すると、「読み取りは守れているのに書き込みが無防備」になるパターンは次のとおりです。
| ポリシーの書き方 | 読み取り(SELECT) | 書き込み(INSERT/UPDATE) | 結果 |
|---|---|---|---|
| FOR SELECT USING(所有権) のみ | 守られる | 書き込みポリシー無し=既定で全拒否 | 書けない(fail-secure。バグではない) |
| FOR INSERT WITH CHECK(true) | 別途SELECT次第 | 新しい行を検査しない | 他テナントのidを差し込める=バイパス |
| FOR UPDATE USING(所有権) WITH CHECK(true) | 別途SELECT次第 | 更新後の行を検査しない | 行のuser_idを書き換えられる=バイパス |
| FOR ALL USING(所有権)(WITH CHECK省略) | 守られる | フォールバックでUSINGを流用 | 所有権列は守られる(ただし他列の昇格は別問題) |
注目してほしいのは1行目です。「書き込みポリシーが無い」のはバイパスではありません。 RLS を有効化してポリシーが1つも無ければ、既定は「全拒否(fail-secure)」。書き込みは静かに弾かれます。問題は、その「弾かれる」状態を雑に解除したときに起きます——次節へ。
3. なぜ書き込み側が無防備になるのか:AI量産と「エラーを黙らせる」誘惑
なぜこの穴がこれほど量産されるのか。理由は2つあります。
3-1. 「new row violates row-level security policy」を黙らせる最短手
RLS を有効化したテーブルに INSERT すると、書き込みポリシーが無い(または WITH CHECK を満たさない)場合に、Supabase/PostgREST はこのエラーを返します。
new row violates row-level security policy for table "documents"
開発者(や生成AIエージェント)がこのエラーに直面したとき、最短で消す方法は with check (true) です。エラーは消え、画面は動き、デモは通ります。しかし第2節で見たとおり、with check (true) は新しい行の検査を完全に無効化する宣言にほかなりません。「動いた」と「安全」が最も鋭く乖離する瞬間です。正しい対処は、true ではなく所有権・テナント帰属を表す述語を書くことです(第4・5節)。
3-2. ハッピーパスの外側はデモで顕在化しない
生成AIは、プロンプトで指示された「やりたいこと(=デモで動くこと)」を最短距離で実装します。「他人の user_id を差し込ませない」「他テナントの行を作らせない」は、明示的に要求されない限りハッピーパスの外側にあり、自分のアカウントで触っている限り誰も踏みません。読み取り側(USING)は画面に出るので意識されますが、書き込み側(WITH CHECK)は目に見えず、後回しにされます。
これは机上の懸念ではありません。2025年登録の CVE-2025-48757 は、AI生成プラットフォーム(Lovable、2025-04-15まで)の不十分なRow-Level Securityポリシーにより、リモートの未認証攻撃者が生成サイトの任意のDBテーブルを読み書きできた、という事例です。分類は CWE-863(Incorrect Authorization)、CVSS基本値 9.3 CRITICAL。「読み書き」と明記されているとおり、RLS不備は読み取りだけでなく書き込みの欠陥でもあります。
そして、書き込みバイパスは「オブジェクト単位の認可」の失敗の一種です。OWASP はこの認可欠陥クラスを API1:2023 Broken Object Level Authorization(BOLA) として API リスクの第1位に置いています。読み取り側の現れ方(他人のデータが見える IDOR)は IDOR/認可欠陥の検出ガイド に、本記事はその書き込み側——他人の所有として行を作る/書き換える——を扱います。
4. 脆弱→修正①:INSERT で他テナントの id を差し込む
最初の攻撃は INSERT です。INSERT は USING を持てず、フォールバックも無いため、WITH CHECK が唯一の関門。ここが true だと、認証済みユーザーは任意の所有者・任意のテナントの行を作れます。
脆弱なポリシー
マルチテナントの documents テーブルを想定します。読み取りは自テナントだけに絞れていますが、挿入の検査がありません。
-- 脆弱:読み取りは自テナントに絞れているが、INSERT の検査が true で無防備
alter table documents enable row level security;
-- 読み取り:自分のテナントの行だけ見える(ここは正しい)
create policy "read own tenant documents"
on documents for select
to authenticated
using ( tenant_id = ((select auth.jwt()) -> 'app_metadata' ->> 'tenant_id')::uuid );
-- 書き込み:ここが穴。新しい行を一切検査していない
create policy "insert documents"
on documents for insert
to authenticated
with check ( true );
攻撃:他テナントの行を作る
documents テーブルは Supabase が PostgREST 経由で REST API として公開します。攻撃者は自分のセッション(authenticated)のまま、tenant_id に他社のIDを入れて挿入するだけです。
// 攻撃:自分は authenticated だが、他テナントの tenant_id を持つ行を差し込む
const { error } = await supabase.from("documents").insert({
title: "汚染ドキュメント",
tenant_id: "00000000-0000-0000-0000-0000000000bb", // ← 自分のテナントではない
body: "他テナントの空間に行を作る",
});
// WITH CHECK(true) は新しい行を検査しないため、これが成功する=書き込みバイパス
挿入された行は他テナントの tenant_id を持つため、被害テナントの画面・集計・通知に紛れ込みます。改ざん・なりすまし投稿・課金データの汚染など、影響はスキーマ次第で深刻です。テナント境界がどこで破れるかの検証手順は マルチテナントのクロステナント漏洩検証 に切り出しています。
修正:新しい行の tenant_id を「呼び出し元のテナント」に縛る
true を、サーバーだけが書ける app_metadata のテナントIDと一致するか検査する述語に置き換えます。user_metadata(ユーザー自身が書き換え可能)を根拠にすると、攻撃者が自分のJWTを書き換えて回避できるので使いません。
-- 修正:新しい行の tenant_id が、呼び出し元のテナントと一致することを強制する
drop policy "insert documents" on documents;
create policy "insert into own tenant"
on documents for insert
to authenticated
with check (
tenant_id = ((select auth.jwt()) -> 'app_metadata' ->> 'tenant_id')::uuid
);
これで、他テナントの tenant_id を持つ行の挿入は new row violates row-level security policy で弾かれます。auth.uid() で個人所有を縛る場合も同型です(with check ( user_id = (select auth.uid()) ))。(select ...) で包むのは Supabase 推奨の最適化で、行ごとの関数再評価を避けて初期計画にキャッシュさせるためです。
5. 脆弱→修正②:UPDATE で user_id を他人に書き換える
次は UPDATE です。UPDATE は USING(更新できる既存行)と WITH CHECK(更新後の行)の両方が効きます。ここで WITH CHECK (true) を書くと、「自分の行しか選べない」のに「書き換えた結果は何でも通る」という、ねじれた穴が開きます。
脆弱なポリシー
-- 脆弱:USING で「自分の行だけ更新対象にできる」が、WITH CHECK(true) で“書ける値”は無制限
create policy "update own profile"
on profiles for update
to authenticated
using ( user_id = (select auth.uid()) )
with check ( true );
攻撃:行の乗っ取り/権限昇格
USING を満たす(=自分が所有する)行を選び、その行の user_id を別人に書き換える、あるいは権限列を昇格させます。
// 攻撃A:自分の行を選び、その user_id を別人に書き換える(行の譲渡・乗っ取り)
await supabase
.from("profiles")
.update({ user_id: "11111111-1111-1111-1111-111111111111" })
.eq("user_id", currentUserId);
// 攻撃B:所有はそのまま、権限列だけ昇格させる
await supabase
.from("profiles")
.update({ role: "admin" })
.eq("user_id", currentUserId);
WITH CHECK(true) は更新後の行を検査しないので、両方とも通ります。
修正:更新後の行も「自分の所有」であることを強制する
-- 修正:USING と WITH CHECK の両方で所有権を縛る(user_id の書き換えを封じる)
create policy "update own profile"
on profiles for update
to authenticated
using ( user_id = (select auth.uid()) ) -- どの既存行を更新できるか
with check ( user_id = (select auth.uid()) ); -- 更新後の行に許す値
正直な注意:フォールバックは“user_id だけ”を守る
ここで誠実に補足します。もし with check (true) を書かず、WITH CHECK を完全に省略していたら、第2節のフォールバックにより USING の user_id = auth.uid() が新しい行にも適用され、攻撃A(user_id の書き換え)は弾かれます。つまりこの攻撃が成立する前提は、with check を省略したのではなく明示的に true(または USING より緩い式)を書いたことにあります。
しかしフォールバックは万能ではありません。using ( user_id = auth.uid() ) がピン留めしているのは user_id 列だけ。攻撃B(role の昇格)は、user_id を変えていないのでフォールバック後も true のまま通ります。 守りたい列が他にもあるなら、WITH CHECK でその列も縛る(例:role = 'user')、あるいは「不変にすべき列はそもそもユーザーに更新させない」設計——列単位の権限(GRANT UPDATE (col) )や、変更を凍結するトリガ、RESTRICTIVE ポリシーの併用——が必要です。WITH CHECK は“新しい行の値”を縛る関門であり、変わってはいけない列をすべて言語化して初めて意味を持ちます。
6. ALL ポリシーの罠と、「読み書きを分ける」設計
実務で最も多いのが、1本の FOR ALL ですべてまかなおうとするパターンです。
-- ALL + USING のみ:フォールバックで INSERT/UPDATE の新しい行にも所有権が効く(この点は安全側)
create policy "owner can do all"
on documents for all
to authenticated
using ( user_id = (select auth.uid()) );
このコード自体は、第2節のフォールバックにより INSERT/UPDATE の新しい行も user_id = auth.uid() で縛られ、所有権の観点では安全側に倒れます。問題は2つです。
- 意図が読み取れない。 書き込みの安全性が「省略時のフォールバック」という暗黙の挙動に依存しているため、レビュアーやAIが後から
with check (...)を足したりfor insert with check (true)を別に生やした瞬間に、誰も気づかず穴が開きます。 - 列の昇格は防げない。
ALLでも、USINGがピン留めしない列(role・tenant_idなど)の書き換えは素通りです(第5節と同じ)。
そこで私が推奨するのは、コマンドごとにポリシーを分け、書き込み系には WITH CHECK を必ず明示することです。冗長に見えても、意図がSQLに表れ、変更が局所化され(ETC:Easy To Change)、レビューで穴を発見しやすくなります。
-- 推奨:読み取りと書き込みを分け、WITH CHECK を“明示”する
create policy "read own documents" on documents for select to authenticated
using ( user_id = (select auth.uid()) );
create policy "insert own documents" on documents for insert to authenticated
with check ( user_id = (select auth.uid()) );
create policy "update own documents" on documents for update to authenticated
using ( user_id = (select auth.uid()) )
with check ( user_id = (select auth.uid()) );
create policy "delete own documents" on documents for delete to authenticated
using ( user_id = (select auth.uid()) );
なお、anon(公開鍵)ロールに書き込みポリシーを与えていないかも要確認です。to authenticated で対象ロールを絞り、未認証の書き込み経路を作らないのが原則です。そして——RLS をどれだけ正しく書いても、service_role キーは BYPASSRLS で RLS を丸ごと飛び越えるため WITH CHECK は一切実行されません。サーバー側で service_role を使う経路の所有権強制は別主題で、IDOR/認可欠陥の検出ガイド に詳述しています。
7. 検出する:migrations の静的検証と実行時の確認
設計で塞ぐと決めたら、「塞げているか」を検証します。書き込みバイパスは、コード(supabase/migrations/**.sql)と実行時の両面から確認できます。
7-1. migrations の静的検証
ポリシー定義SQLを読み、次の“形”を機械的に洗い出します。
- 書き込み可能なポリシー(
FOR INSERT/FOR UPDATE/FOR ALL)で、**WITH CHECKがtrue**になっている FOR INSERTポリシーなのにWITH CHECKが無い(挿入を許可しているのに新しい行を検査していない)WITH CHECKがUSINGより緩い(読めない行を書ける=非対称)- 所有権列(
user_id/tenant_id等)をWITH CHECKが参照していない
これは正規表現で雑に grep するより、ポリシーの構造を解析して USING と WITH CHECK を突き合わせるほうが確実です。私が公開しているOSS Aegis は supabase/migrations を解析してこれらを検出します。インストール不要で走ります。
# インストール不要・設定不要でスキャン(WITH CHECK の欠落・true・USING との不一致を検出)
npx @aegiskit/cli scan
RLS設定ミス全般(RLS未有効化、using (true)、search_path 未固定の SECURITY DEFINER、anon への過剰GRANT など)の体系的な検出は RLS設定ミスの検出と監査 にまとめています。
7-2. 実行時に「書けないこと」を回帰テストにする(pgTAP)
静的検証は“疑う”ところまで。最後は、別ユーザー/別テナントのコンテキストで書き込みが拒否されることを実際に確かめ、CIで退行を防ぎます。pgTAP なら、他テナントの id を差し込む INSERT が RLS で弾かれることをテストにできます。
-- pgTAP:他テナントの id を差し込む INSERT が RLS で拒否されることを回帰テストにする
begin;
select plan(1);
set local role authenticated;
set local request.jwt.claims to
'{"sub":"user-a","app_metadata":{"tenant_id":"00000000-0000-0000-0000-0000000000aa"}}';
select throws_ok(
$$ insert into documents (title, tenant_id)
values ('x', '00000000-0000-0000-0000-0000000000bb') $$,
'42501', -- insufficient_privilege(RLS の WITH CHECK 違反)
null,
'authenticated user cannot insert a row for another tenant'
);
select * from finish();
rollback;
UPDATE 側も同様に、「自分の行の user_id を別人に書き換える更新が弾かれること」「role を昇格させる更新が弾かれること」をそれぞれ1本ずつ書いておくと、第5節の2つの攻撃に対する回帰ガードになります。
7-3. 正直なスコープ
ここは強調させてください。いかなる静的・動的ツールも、あなたの認可が正しいことは証明しません。 ツールが検出できるのは「WITH CHECK が無い/true だ/USING と非対称だ」という形であって、「tenant_id = app_metadata の tenant_id という述語が、このシステムのテナント帰属の定義として正しいか」という意味ではありません。クリーンな結果は「よくある罠は踏んでいない」であって「安全」ではない。これらの検証は、人間のレビューと脅威モデリングを置き換えるものではなく、補完するものです。
8. 本番前チェックリスト
外注でもAI製でも、本番投入の前に最低限これだけは確認してください。
- 書き込みを許可する全ポリシー(
FOR INSERT/FOR UPDATE/FOR ALL)にWITH CHECKが明示されている -
with check (true)がどこにも無い(エラーを黙らせるためのtrueが残っていないか) -
FOR INSERTポリシーのWITH CHECKが、所有権・テナント列(user_id/tenant_id)を呼び出し元に縛っている -
FOR UPDATEのWITH CHECKが、更新後の行の所有権列を縛り、user_idの書き換えを封じている - テナント/所有の根拠が
app_metadata等サーバー権限の値で、user_metadata(ユーザー書き換え可能)に依存していない - 変えてはいけない列(
role・tenant_id・is_admin等)が、WITH CHECKか列権限かトリガで保護されている -
anonロールに不要な書き込みポリシーを与えていない(to authenticatedで絞る) - **2アカウント/2テナントで「他人として書く」**を実際に試し、
INSERT/UPDATEが拒否されることを確認した - pgTAP 等で「書けないこと」をCIの回帰テストにしている
-
service_roleを使う書き込み経路は、RLS を飛び越える前提でコード側で所有権を強制している
発注者の視点で最も効くのは、**「他人の user_id で行を作れますか?」「with check は何を検査していますか?」「読めない行を書けないことを、どう確認しましたか?」**の3問です。良い開発者は即答できます。
9. どこまで自分で、どこから監査か
最後に、正直に線を引きます。
“形”の検出は自動化できます。 WITH CHECK の欠落・true・USING との非対称は、npx @aegiskit/cli scan のような静的検証で機械的に拾えます。まずは Aegis(無料OSS)で現状を可視化するのが、最もコスパの良い第一歩です。書き込みポリシーに with check (true) が一つでも残っていないか——それを一覧化するだけでも、最頻出の事故はかなり防げます。
一方、“意味”の正しさは人間の領域です。 「このテーブルのテナント帰属を何で決めるか」「どの列は誰にも書き換えさせないか」「更新で許してよい状態遷移は何か」は、あなたのデータモデルと事業ルールを理解した人間にしか判断できません。ここで「導入すれば書き込みは安全」と言い切る製品は、むしろ危険です。Aegis は WITH CHECK の欠落・true・非対称を検出・警告しますが、その述語が業務的に正しいことは証明しません。完全に安全にする魔法はありません。
だからこそ線引きが要ります。どこまで自分で固め、どこから専門家のレビューが必要か——WITH CHECK の述語設計、テナント帰属の根拠、不変列の保護といった書き込み側の認可設計のレビューが必要なら、セキュリティ監査で承ります。私自身、木材流通業界のDX案件で、マルチテナントの読み書き双方の RLS とテナント分離・所有権強制を実運用で設計・検証してきました。
よくある質問(FAQ)
Q. RLS を有効化して USING を書けば、書き込みも守られますか?
A. いいえ。USING は読み取り(既存行)のフィルタです。INSERT は USING を持てず、書き込みの安全性は WITH CHECK だけに懸かっています。UPDATE は省略時にフォールバックで USING が流用されますが、with check (true) を書けば消え、USING がピン留めしない列の昇格も防げません。読み取りと書き込みは別の関門だと考えてください。
Q. with check (true) はなぜ危険なのですか?
A. 「新しい行を一切検査しない」と宣言しているからです。INSERT/UPDATE 時の new row violates row-level security policy エラーを最短で消せるため安易に使われますが、それは保護を外しているだけです。true ではなく所有権・テナントを表す述語を書いてください。
Q. UPDATE で WITH CHECK を省略するのは危険ですか?
A. 省略時はフォールバックで USING が新しい行にも適用されるため、USING がピン留めする列(例:user_id)の書き換えは弾かれます。ただしそれに頼るのは危険です。USING が縛らない列(role など)は素通りしますし、暗黙の挙動はレビューで見落とされやすい。書き込み系は WITH CHECK を明示し、変えてはいけない列をすべて言語化するのが安全です。
Q. service_role を使えば WITH CHECK は関係ない?
A. 関係ない、というより危険な意味で無関係です。service_role は BYPASSRLS で RLS を完全に飛び越えるため、WITH CHECK は実行されません。サーバー側で service_role を使う経路では、RLS に頼れない前提でコード側に所有権・テナントの強制を書く必要があります(IDOR/認可欠陥の検出ガイド)。
Q. ツールを入れれば書き込みバイパスは無くなりますか?
A. なくなりません。静的検証は WITH CHECK の欠落・true・USING との非対称という“形”を検出・警告できますが、述語が業務的に正しいかは判断しません。検出(ツール)と設計(人間)と検証(テスト)の三つで初めて本番品質になります。
まとめ:読み取りを縛っても、書き込みは縛られない
要点を整理します。
- RLS には条件式の置き場所が2つある。
USINGは「どの既存行が見えるか」を決める読み取りフィルタ、WITH CHECKは「どの新しい行を書けるか」を決める書き込みフィルタ。SELECT/DELETE はUSINGのみ、INSERT はWITH CHECKのみ、UPDATE は両方が効く。 WITH CHECKを欠いた(またはtrueにした)書き込みポリシーは『書き込みバイパス』を開く。INSERT で他テナントのidを差し込み、UPDATE で行のuser_idを他人に書き換えられる。読み取りのテストでは見えない。UPDATE/ALLのフォールバック(省略時にUSINGを流用)は安全網だが頼ってはいけない。INSERT には無く、with check (true)で消え、USINGがピン留めしない列は守れない。- 量産の引き金は「
new row violates row-level security policyをwith check (true)で黙らせる」誘惑と、ハッピーパス外で顕在化しない書き込み欠陥。CVE-2025-48757 はその深刻な実例。 - 検出は migrations の静的検証(
npx @aegiskit/cli scan)+ pgTAP で「書けないこと」を回帰テスト化。ただしツールは“形”を検出するだけで、述語の“正しさ”は人間の設計とレビューでしか守れない。
AIで速く作ること自体は正しい。速く作ったものの“書き込み側”を、漏らさず固める——その WITH CHECK 設計や、既存の Supabase アプリの読み書き双方の RLS レビューが必要であれば、お気軽にご相談ください。
参考資料
- PostgreSQL — CREATE POLICY(USING と WITH CHECK、コマンド別の適用、省略時のフォールバック)
- PostgreSQL — Row Security Policies(RLSの全体像とポリシーの挙動)
- Supabase Docs — Row Level Security(service_roleはRLSをバイパスする)
- NVD — CVE-2025-48757(不十分なRLSによる未認証の読み書き、CWE-863、CVSS 9.3)
- OWASP API1:2023 — Broken Object Level Authorization(BOLA/IDOR、API最頻出リスク)