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で両方を呼び出しました。
| 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警告:意味と対処にあります。