Zum Hauptinhalt springen
友田 陽大
Application-layer security
Supabase
RLS
PostgreSQL
セキュリティ

Supabase RLSの WITH CHECK 欠落で起きる『書き込みバイパス』 — USING との違いと、INSERT/UPDATE を正しく守る

SupabaseのRLSで混同しやすいUSING(読み取りフィルタ)とWITH CHECK(書き込みフィルタ)の違いを整理し、WITH CHECKを欠いた書き込みポリシーが認証済みユーザーに他人のuser_idや他テナントのidを持つ行を作らせる『書き込みバイパス』を、INSERT/UPDATE/ALLの脆弱→修正SQLで解説します。

Published
Reading time
22 min read
Author
友田 陽大
Teilen

最初に結論を述べます。Supabase(PostgreSQL)の行レベルセキュリティ(RLS)でいちばん事故が多いのは、USINGWITH CHECK の混同です。USING は「どの“既存行”が見えるか」を決める読み取りフィルタ、WITH CHECK は「どの“新しい行”を書いてよいか」を決める書き込みフィルタで、両者はまったく別の関門です。 読み取り側(USING)だけを丁寧に書き、書き込み側(WITH CHECK)を欠いた——あるいは with check (true) で黙らせた——ポリシーは、**認証済みの正規ユーザーが「他人の user_id を持つ行」や「他テナントの id を差し込んだ行」を作れてしまう『書き込みバイパス』**を開きます。

これは直感に反するねじれた事故です。「自分の行しか読めない」テーブルに、「他人の行を書ける」という穴が同居する。読み取りのテストは通り、デモでも顕在化せず、本番で初めて——あるいは攻撃者にだけ——露見します。本記事は、USINGWITH CHECK の違いをコマンド別に正確に押さえ、INSERT/UPDATE/ALL の脆弱SQLを実際の攻撃とともに分解し、修正SQLと検出・検証の手順まで、PostgreSQL/Supabase の一次情報に基づいて解説します。これは Next.js × Supabase アプリケーションセキュリティ完全ガイド で「垂直リスク(設計でしか守れない認可)」と呼んだ層を、書き込み側に絞って深掘りするものです。


1. 結論を分解する:USING と WITH CHECK は別の関門

RLS のポリシーには、条件式を書く場所が2つあります。USINGWITH CHECK です。役割は明確に分かれています(PostgreSQL: CREATE POLICY)。

  • USING(既存行の検査) — すでにテーブルに存在する行に対して評価され、true になった行だけが「見える/操作対象にできる」。これは読み取り側のフィルタです。
  • WITH CHECK(新しい行の検査)INSERTUPDATE で生まれる新しい行の内容に対して評価され、false(または null)になればエラーで弾かれる。これは書き込み側のフィルタです。公式は明記しています——「check_expression は行の元の内容ではなく提案された新しい内容に対して評価される」。

つまり USING は「どの行を見せるか」、WITH CHECK は「どんな行を書かせるか」。読み取りを縛っても書き込みは縛られません。 この一点が、本記事のすべてです。

どのコマンドでどちらが効くかを表にします。これは PostgreSQL: CREATE POLICY の「Policies Applied by Command Type」に対応します。

コマンドUSING(既存行に対する検査)WITH CHECK(新しい行に対する検査)
SELECT効く(見える行を絞る)指定できない
INSERT指定できない(既存行が無い)効く(作る行を検査する)
UPDATE効く(更新できる既存行を絞る)効く(更新後の行を検査する)
DELETE効く(削除できる既存行を絞る)指定できない
ALL効く効く

ここから読み取れる重要な事実は3つです。

  1. SELECTDELETEUSING だけ。読む・消すは「既存行」の話なので、新しい行の検査(WITH CHECK)は出番がありません。
  2. INSERTWITH CHECK だけ。既存行が無いので USING指定すらできません(公式:「INSERT ポリシーは USING 式を持てない」)。つまり挿入の安全性は WITH CHECK ただ一つに懸かっている
  3. UPDATE は両方効く。「どの既存行を更新できるか」を USING で、「更新後の行に何を許すか」を WITH CHECK で、二段で縛ります。

RLS そのものの位置づけは Supabase: Row Level SecurityPostgreSQL: Row Security Policies が一次情報です。RLS を「有効化した」ことと「読み書きの両方が正しく効いている」ことは別物——本記事はその後半、書き込み側の検証を扱います。


2. UPDATE/ALL のフォールバックという“落とし穴つきの安全網”

ここで、多くの解説が省く重要な挙動を正確に押さえます。UPDATEALL のポリシーで WITH CHECK を省略すると、PostgreSQL は USING 式を WITH CHECK にも流用します。 公式の言葉です——「USINGWITH CHECK の両方を持てるポリシー(ALLUPDATE)で 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 は縛りますが、roletenant_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 です。INSERTUSING を持てず、フォールバックも無いため、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 です。UPDATEUSING(更新できる既存行)と 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節のフォールバックにより USINGuser_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つです。

  1. 意図が読み取れない。 書き込みの安全性が「省略時のフォールバック」という暗黙の挙動に依存しているため、レビュアーやAIが後から with check (...) を足したり for insert with check (true) を別に生やした瞬間に、誰も気づかず穴が開きます。
  2. 列の昇格は防げない。 ALL でも、USING がピン留めしない列(roletenant_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 CHECKtrue**になっている
  • FOR INSERT ポリシーなのに WITH CHECK が無い(挿入を許可しているのに新しい行を検査していない)
  • WITH CHECKUSING より緩い(読めない行を書ける=非対称)
  • 所有権列(user_id / tenant_id 等)を WITH CHECK が参照していない

これは正規表現で雑に grep するより、ポリシーの構造を解析して USINGWITH CHECK を突き合わせるほうが確実です。私が公開しているOSS Aegis は supabase/migrations を解析してこれらを検出します。インストール不要で走ります。

# インストール不要・設定不要でスキャン(WITH CHECK の欠落・true・USING との不一致を検出)
npx @aegiskit/cli scan

RLS設定ミス全般(RLS未有効化、using (true)search_path 未固定の SECURITY DEFINERanon への過剰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 UPDATEWITH CHECK が、更新後の行の所有権列を縛りuser_id の書き換えを封じている
  • テナント/所有の根拠が app_metadata 等サーバー権限の値で、user_metadata(ユーザー書き換え可能)に依存していない
  • 変えてはいけない列roletenant_idis_admin 等)が、WITH CHECK か列権限かトリガで保護されている
  • anon ロールに不要な書き込みポリシーを与えていない(to authenticated で絞る)
  • **2アカウント/2テナントで「他人として書く」**を実際に試し、INSERT/UPDATE が拒否されることを確認した
  • pgTAP 等で「書けないこと」をCIの回帰テストにしている
  • service_role を使う書き込み経路は、RLS を飛び越える前提でコード側で所有権を強制している

発注者の視点で最も効くのは、**「他人の user_id で行を作れますか?」「with check は何を検査していますか?」「読めない行を書けないことを、どう確認しましたか?」**の3問です。良い開発者は即答できます。


9. どこまで自分で、どこから監査か

最後に、正直に線を引きます。

“形”の検出は自動化できます。 WITH CHECK の欠落・trueUSING との非対称は、npx @aegiskit/cli scan のような静的検証で機械的に拾えます。まずは Aegis(無料OSS)で現状を可視化するのが、最もコスパの良い第一歩です。書き込みポリシーに with check (true) が一つでも残っていないか——それを一覧化するだけでも、最頻出の事故はかなり防げます。

一方、“意味”の正しさは人間の領域です。 「このテーブルのテナント帰属を何で決めるか」「どの列は誰にも書き換えさせないか」「更新で許してよい状態遷移は何か」は、あなたのデータモデルと事業ルールを理解した人間にしか判断できません。ここで「導入すれば書き込みは安全」と言い切る製品は、むしろ危険です。Aegis は WITH CHECK の欠落・true・非対称を検出・警告しますが、その述語が業務的に正しいことは証明しません。完全に安全にする魔法はありません。

だからこそ線引きが要ります。どこまで自分で固め、どこから専門家のレビューが必要か——WITH CHECK の述語設計、テナント帰属の根拠、不変列の保護といった書き込み側の認可設計のレビューが必要なら、セキュリティ監査で承ります。私自身、木材流通業界のDX案件で、マルチテナントの読み書き双方の RLS とテナント分離・所有権強制を実運用で設計・検証してきました。


よくある質問(FAQ)

Q. RLS を有効化して USING を書けば、書き込みも守られますか? A. いいえ。USING は読み取り(既存行)のフィルタです。INSERTUSING を持てず、書き込みの安全性は WITH CHECK だけに懸かっています。UPDATE は省略時にフォールバックで USING が流用されますが、with check (true) を書けば消え、USING がピン留めしない列の昇格も防げません。読み取りと書き込みは別の関門だと考えてください。

Q. with check (true) はなぜ危険なのですか? A. 「新しい行を一切検査しない」と宣言しているからです。INSERT/UPDATE 時の new row violates row-level security policy エラーを最短で消せるため安易に使われますが、それは保護を外しているだけです。true ではなく所有権・テナントを表す述語を書いてください。

Q. UPDATEWITH CHECK を省略するのは危険ですか? A. 省略時はフォールバックで USING が新しい行にも適用されるため、USING がピン留めする列(例:user_id)の書き換えは弾かれます。ただしそれに頼るのは危険です。USING が縛らない列(role など)は素通りしますし、暗黙の挙動はレビューで見落とされやすい。書き込み系は WITH CHECK を明示し、変えてはいけない列をすべて言語化するのが安全です。

Q. service_role を使えば WITH CHECK は関係ない? A. 関係ない、というより危険な意味で無関係です。service_roleBYPASSRLS で RLS を完全に飛び越えるため、WITH CHECK は実行されません。サーバー側で service_role を使う経路では、RLS に頼れない前提でコード側に所有権・テナントの強制を書く必要があります(IDOR/認可欠陥の検出ガイド)。

Q. ツールを入れれば書き込みバイパスは無くなりますか? A. なくなりません。静的検証は WITH CHECK の欠落・trueUSING との非対称という“形”を検出・警告できますが、述語が業務的に正しいかは判断しません。検出(ツール)と設計(人間)と検証(テスト)の三つで初めて本番品質になります。


まとめ:読み取りを縛っても、書き込みは縛られない

要点を整理します。

  • 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 policywith check (true) で黙らせる」誘惑と、ハッピーパス外で顕在化しない書き込み欠陥。CVE-2025-48757 はその深刻な実例。
  • 検出は migrations の静的検証(npx @aegiskit/cli scan)+ pgTAP で「書けないこと」を回帰テスト化。ただしツールは“形”を検出するだけで、述語の“正しさ”は人間の設計とレビューでしか守れない

AIで速く作ること自体は正しい。速く作ったものの“書き込み側”を、漏らさず固める——その WITH CHECK 設計や、既存の Supabase アプリの読み書き双方の RLS レビューが必要であれば、お気軽にご相談ください。


参考資料

友田

友田 陽大

Entwickler eines mit dem METI-Ministerpreis ausgezeichneten Produkts. Mit TypeScript + Python + AWS liefere ich SaaS, Industrie-Digitalisierung und produktionsreife generative KI (RAG) durchgängig — von der Anforderung über die Infrastruktur bis zum Betrieb, allein.

The vulnerabilities in this article — is your app safe from them?

An expert audit of your Next.js × Supabase authorization & RLS

The IDOR, RLS misconfigurations, and tenant-boundary crossing covered here are vertical risks a library can't fix. I take it on as a security audit — from authorization review through fix design and implementation. You're welcome to visualize the current state with the free OSS first.

Verfügbar für Projektarbeit (Werkvertrag) und als technischer Berater. Beginnen Sie mit einem kostenlosen 30-minütigen Gespräch.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Ebenfalls lesenswert