本文へスキップ

Supabase security definer view:RLS迂回の理由

Supabase

Markdownで表示Claudeで開くChatGPTで開くPerplexityで開く

質問もコピーされます。チャットが空で開いた場合は貼り付けてください。

TL;DR

Postgresのビューは、security_invoker = trueを付けて作らない限り、所有者の権限で実行されます。Supabaseでは所有者は通常postgresで、RLSを迂回します。そのため、保護されたテーブルの上に作ったビューは、テーブル自体が隠す行を返します。私のローカルスタックでは、公開キーはテーブルから0行、その上の通常のビューから3行中3行を取得しました。security_invoker = trueを付けると、ビューも0行を返しました。Postgres 15以降では、公開スキーマのすべてのビューに設定してください。

私の無料のSupabaseセキュリティチェッカーには「RLS を迂回するビュー」というルールがあり、SupabaseのSecurity Advisorは同じ問題をsecurity_definer_viewとして報告します。漏れ元のテーブルは保護されているように見えるので、見落としやすい問題です。

Supabaseのビューはなぜ RLS を迂回する?

ビューは保存されたクエリです。デフォルトでは、Postgresはビューの中のテーブルの権限とRLSを、呼び出した人ではなくビューの所有者として確認します。SupabaseのRLSガイドははっきり書いています。ビューは通常postgresユーザーで作られるため、デフォルトではRLSを迂回し、保護されたテーブルの上のビューは、ポリシーが止めるはずだったすべての行を渡してしまいます。

Data APIはビューをテーブルと同じように提供します。ビューが公開スキーマにあり、anonやauthenticatedがそれにselectできるなら、公開キーで読めます。

私のテストでビューは何を返した?

ローカルのSupabaseスタック(CLI 2.109.1、Postgres 17.6、PostgREST 14.14)で、架空の顧客3人を入れたテーブルordersを作り、ポリシーなしでRLSを有効にしました。その上に、anonへのselectのgrant付きでビューorder_summaryを作りました。そして、publishable keyで両方を呼び出しました。

同じテーブルを読む3つの方法。ローカルのSupabase、RLS有効でポリシーなし、publishable key。テーブル自体:Publishable key → orders、RLS有効 → 0行、その上の通常のビュー:Publishable key → order_summaryビュー → 3行中3行、security_invokerのビュー:Publishable key → security_invokerビュー → 0行。
ローカルのSupabaseスタックでの私の再現、2026年10月23日。
publishable keyでのリクエスト 結果
GET /rest/v1/orders(RLS有効、ポリシーなし) []
GET /rest/v1/order_summary(通常のビュー) 名前とメール付きの全3行
security_invoker = trueにしたあとのGET /rest/v1/order_summary []

テーブルも同じ、キーも同じです。返ってくるものを変えたのは、ビューの設定だけでした。

どう直す?

Postgres 15以降(現在のSupabaseプロジェクトはすべてこれです)では、ビューが呼び出し元としてRLSを確認するようにします。

alter view public.order_summary set (security_invoker = true);

新しいビューなら、定義に含めます。

create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;

こうすると、呼び出し元は元のテーブルに対するselectも必要になり、どの行を返すかはテーブルのポリシーが決めます。公開の件数のように、呼び出し元が行単位では見られないデータをビューで集計する必要がある場合は、公開スキーマの外に置き、結果を絞った関数を通して提供してください。

publicにある、まだ所有者として実行されているビューを一覧にするには、次のクエリを使います。

select c.relname as view
from pg_class c join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'v'
  and not coalesce(
    (select bool_or(option = 'security_invoker=true' or option = 'security_invoker=on')
     from unnest(c.reloptions) as option), false);

私のスタックでは、通常のビュー1つとsecurity_invoker = trueのビュー2つがある状態で、通常のものだけが返りました。

マテリアライズドビューはRLSにまったく対応していないため、チェッカーは別のルール「API に公開されたマテリアライズドビューまたは外部テーブル」で指摘します。secret keyからSECURITY DEFINER関数まで、ほかにポリシーをすり抜けるものはLovableアプリのセキュリティチェックリストにあり、ポリシー自体を証明する方法はSupabase RLSのテスト方法にあります。ヘルパー関数でチームを引き、再帰を起こさないポリシーは、Supabase RLSポリシー例:APIで検証済みにあります。security definer関数にはもう1つ、固定されたsearch_pathという設定が必要です。私のテストでsearch_path = publicでは足りなかった理由は、Supabase search_path警告:意味と対処にあります。

© 2026 Denys Zaichenko. 元の記事: https://madsalt.tech/ja/notes/supabase-security-definer-view/