Chuyển đến nội dung chính
友田 陽大
Databases & RLS
Supabase
RLS
PostgreSQL
セキュリティ
テスト

Supabase RLSが効かない・空が返る・INSERTが弾かれる:原因別トラブルシューティング完全ガイド

Supabase(PostgreSQL)の行レベルセキュリティ(RLS)でよく遭遇する3大症状——『SELECTが空で返る』『new row violates row-level security policyでINSERTが弾かれる』『RLSが効かずデータが漏れる』——を、原因の切り分けフローと診断SQL(pg_policies・relrowsecurity・auth.uid()・set local role)で体系的にデバッグします。公式準拠で、推測ではなく証拠で直す手順。

Published
Reading time
10 min read
Author
友田 陽大
Chia sẻ

RLS(行レベルセキュリティ)のデバッグで最もやってはいけないのは、「とりあえずポリシーを using (true) にして動くか試す」ことです。それは「鍵が開かないから玄関を外した」のと同じで、動いたように見えて全データを公開しています。トラブルシューティングの原則は一つ——推測でポリシーをいじらず、まず『どこで止まっているか』を証拠で特定する

この記事は、Supabase + PostgreSQL の RLS で遭遇する3大症状を、原因別に切り分けて直す実践ガイドです。

  1. SELECT が空で返る(読めるはずが0件)
  2. new row violates row-level security policy で INSERT が弾かれる
  3. RLS が効かず、データが漏れる(見えてはいけない行が見える)

題材はリアルタイム試合記録アプリ(69テーブル全RLS・280ポリシー)の運用知見。内容はSupabase公式PostgreSQL公式(2026年6月時点)に忠実です。

RLSの前提(有効化・USING/WITH CHECK・ロール)はRLS入門、Next.js特有の「クライアント生成ミスで空が返る」はApp Router統合ガイドで扱っています。本記事はDB側の切り分けに集中します。


0. まず手に入れる:4つの診断クエリ

症状を見る前に、証拠を集める道具を用意します。この4つで「RLSの状態」が丸裸になります。

診断1:RLSは有効か?所有者バイパスは?

select relname,
       relrowsecurity   as rls_enabled,   -- enable row level security 済みか
       relforcerowsecurity as rls_forced  -- 所有者にも強制しているか
from pg_class
where oid = 'public.matches'::regclass;
  • rls_enabled = false なら、RLSはそもそも効いていません(症状3の最頻原因)。
  • rls_enabled = true, rls_forced = false なら、テーブル所有者として繋ぐとバイパスされます。

診断2:どんなポリシーが付いているか?

select policyname, cmd, roles, qual as using_expr, with_check as check_expr
from pg_policies
where schemaname = 'public' and tablename = 'matches';

qual(USING)と with_check(WITH CHECK)の中身、対象 cmd(SELECT/INSERT/...)、roles(TO句)が一覧で見えます。「ポリシーが0件」ならデフォルト拒否で全て弾かれます。

診断3:いま自分は誰か?auth.uid()は何を返すか?

select current_user,                          -- 接続ロール(anon/authenticated/...)
       auth.uid()       as uid,               -- ユーザーID(未認証ならnull)
       auth.jwt()->>'role' as jwt_role;        -- JWTのrole

uidnull なら未認証扱いで、(select auth.uid()) = user_id は永遠に偽になります。

診断4:認証済みユーザーになりきって再現する

ダッシュボードのSQLエディタは通常 service_role(RLSバイパス)で動くため、バグを再現できません。特定ユーザーに偽装します。

begin;
  set local role authenticated;
  set local request.jwt.claims =
    '{"sub":"00000000-0000-0000-0000-000000000001","role":"authenticated"}';

  -- ここで本番と同じクエリを投げ、症状を再現する
  select * from public.matches;
rollback; -- 状態を変えない(冪等)

この4つが揃えば、あとは症状ごとに「どの診断が異常か」を見るだけです。


1. 症状A:SELECTが空で返る(読めるはずなのに0件)

最頻の悩みです。原因は次の4つに収束します。診断1〜4で順に潰します。

原因診断での見え方直し方
ポリシー不在(有効化だけした)診断2が0件SELECTポリシーを1本書く
未認証(JWTが届いていない)診断3で uidnull認証経路を直す(Next.jsは統合ガイド
GRANT不足permission denied for table エラーgrant select ... to authenticated
USINGの述語不一致診断4でも0件述語の列・型・値を実データと突き合わせる

切り分けの順番

  1. 診断2でポリシーを見る。 0件なら答えは出た——enable したのにSELECTポリシーが無く、デフォルト拒否で全部消えている。
  2. 診断3で uid を見る。 null なら未認証。アプリ側はトークンを送れているか(Next.jsならクライアント生成・middleware)。
  3. 診断4で認証済みになりきる。 ここで正しく行が返るなら、問題はDBではなくアプリの認証経路。逆に診断4でも空なら、ポリシーの述語が間違っている。
  4. 述語不一致の典型:user_id に入っているのが auth.users.id ではなく別IDだった、型が text vs uuid でキャストが必要だった、tenant_id の値がずれていた——実データを select user_id from matches limit 5 で見て、auth.uid() の値と照合します。

やってはいけない切り分けservice_role で繋いで「データはある」と確認して満足すること。service_role はRLSをバイパスするので「データの存在」しか分からず、RLSの問題は何一つ切り分けられません。必ず診断4で authenticated として再現してください。


2. 症状B:new row violates row-level security policy

INSERT/UPDATE時のこのエラー(SQLSTATE 42501)は、WITH CHECK 違反です。意味は明確——「作ろうとしている新しい行が、書き込みポリシーの述語を満たしていない」。

PostgreSQL公式の定義通り、WITH CHECK は行の元の内容ではなく提案された新しい内容に対して評価され、偽なら弾かれます(PostgreSQL公式)。これはバグではなくRLSが正しく仕事をしているサインなので、「黙らせる」のではなく「なぜ満たさないか」を見ます。

よくある3つの原因

-- 想定ポリシー
create policy "insert own" on public.matches for insert
to authenticated
with check ( (select auth.uid()) = user_id );
  1. user_id を入れ忘れている(最頻)

    // ❌ user_id が null → (select auth.uid()) = null は偽 → 弾かれる
    await supabase.from("matches").insert({ opponent: "A", score: "3-1" });
    // ✅ user_id を明示する
    await supabase.from("matches").insert({ opponent: "A", score: "3-1", user_id: user.id });
    

    DBのデフォルト値で user_id default auth.uid() を設定しておくと、入れ忘れを構造的に防げます。

  2. 他人/他テナントのIDを差し込んでいるuser_id に別ユーザーのIDを入れている。これはポリシーが正しく防いでいるケース。アプリのバグを直す。

  3. UPDATEで「更新後の行」が境界を越える:UPDATEは USING(更新できる既存行)と WITH CHECK(更新後の行)の両方が効きます。tenant_id を別テナントに書き換えるUPDATEは WITH CHECK で弾かれる。これも正しい挙動。

絶対にやってはいけない「解決」

-- ❌❌ これは解決ではなく、書き込みバイパスを開く穴
create policy "insert own" on public.matches for insert
to authenticated with check ( true );

with check (true) でエラーは消えますが、誰でも任意の user_id・任意の tenant_id の行を作れるようになります。これは典型的な書き込みバイパス脆弱性で、AI生成コードや短納期で量産される最悪パターンです。エラーは「黙らせる」のではなく「述語を満たすデータを送る」で直してください。


3. 症状C:RLSが効かず、データが漏れる(最も危険)

症状AとBは「厳しすぎる」問題ですが、Cは逆——緩すぎて見えてはいけない行が見える。これは事故として最悪です。原因は次の通り。診断1(relrowsecurity)から疑います。

原因診断直し方
enable忘れ診断1で rls_enabled = falsealter table ... enable row level security
service_roleで接続診断3で current_user = service_roleクライアントは anon/publishableキーへ
所有者バイパス診断1で rls_forced = false かつ所有者接続alter table ... force row level security
using (true) の置き忘れ診断2のUSINGが true正しい述語に直す
ビューがRLSをバイパスビュー経由で全行見えるsecurity_invoker = true(PG15+)
SECURITY DEFINER関数の穴関数経由で全行返るsearch_path=''・最小権限で固める

enable忘れ:全テーブルを一括監査する

1テーブルずつ目視するのは漏れます。公開スキーマでRLS未有効のテーブルを一覧にして、CIのブロッキング条件にします。

-- public スキーマで RLS が無効なテーブル=潜在的に全公開
select tablename
from pg_tables
where schemaname = 'public'
  and tablename not in (
    select c.relname from pg_class c
    join pg_namespace n on n.oid = c.relnamespace
    where n.nspname = 'public' and c.relrowsecurity = true
  );

これが1行でも返ったら、そのテーブルはGRANTの範囲で全公開の可能性があります。「enable 忘れの1テーブル」はSaaSで最も多い重大漏洩経路です。

ビューの落とし穴

ビューはデフォルトで作成者の権限で実行され、RLSをバイパスします。public_matches のようなビューを anon に見せていると、裏のRLSを素通りして全行漏れます。PostgreSQL 15+ では security_invoker呼び出し元の権限=RLSを尊重させます。

create view public.match_summary
with (security_invoker = true)   -- ビューでもRLSを効かせる
as select id, opponent, score from public.matches;

SECURITY DEFINER関数の穴

複雑な認可を security definer 関数に逃がすと、その関数はRLSをバイパスして実行されます。便利な反面、search_path を固定せず・部外者にも実行を許すと、全行を返す穴になります。set search_path = ''・完全修飾・最小権限で固めること(詳細)。


4. 切り分けフロー(保存版)

3症状をまたいだ一枚の判断フローです。詰まったらここに戻ってください。

データが「見えない」?
├─ 診断2: ポリシーは在る? → 無 → SELECTポリシーを書く
├─ 診断3: auth.uid()はnull? → はい → 認証経路(JWT伝播)を直す
├─ 診断4: 認証済み偽装で返る? → はい → 問題はアプリ側。DBは正常
│                              → いいえ → USING述語の列/型/値を実データと照合
└─ permission deniedエラー → GRANT不足

INSERT/UPDATEが「弾かれる」?(42501)
└─ WITH CHECK違反 → 送る行の user_id/tenant_id を点検(true で黙らせない)

データが「漏れる」?
├─ 診断1: rls_enabled=false → enableする(全テーブル監査)
├─ 診断3: service_role接続 → anonキーへ
├─ 診断1: rls_forced=false+所有者接続 → forceする
├─ ビュー経由 → security_invoker=true
└─ definer関数経由 → search_path=''・最小権限

5. 直したら必ず:許可と拒否の両方をテストで固定する

トラブルシューティングは述語を書き換える作業です。だから最後に必ず、「許可されるべきが許可される」と「拒否されるべきが拒否される」の両方を自動テストで固定します。特に症状B・Cを直した後は、述語を緩めて「動いたが漏れる」退行を作っていないかを、拒否テストで確かめてください。

-- pgTAP例:部外者には1行も見えないことを「拒否」として固定する
begin;
  select plan(1);
  set local role authenticated;
  set local request.jwt.claims = '{"sub":"99999999-9999-9999-9999-999999999999"}';
  select is(
    (select count(*) from public.matches)::int, 0,
    '部外者には他人のmatchesが1行も見えない'
  );
  select * from finish();
rollback;

拒否テストの書き方とCIゲート化はpgTAPでRLSを守る記事で詳述しています。「直った」をデモではなくテストで証明する——これがトラブルシューティングを「もぐら叩き」から「恒久対策」に変える最後の一手です。


まとめ:推測で直さず、証拠で直す

  • RLSのデバッグは relrowsecurity / pg_policies / auth.uid() / set local role の4診断で「どこで止まっているか」を証拠で特定してから直す。
  • 空で返る=ポリシー不在・未認証・GRANT不足・述語不一致。service_role で確認して満足しない。
  • new row violates ...(42501)=WITH CHECK違反。with check(true) で黙らせず、送るデータを直す。
  • 漏れる=enable忘れ・service_role接続・所有者バイパス・ビュー/definer関数のバイパス。relrowsecurity=false をまず疑う。
  • 直したら許可と拒否の両方をpgTAPで固定し、「動いたが漏れる」退行を出さない。

一次情報(必ず最新を確認してください)

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

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

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

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

Cũng đáng đọc