TL;DR
View v Postgresu běží s právy svého vlastníka, pokud není vytvořeno s security_invoker = true. V Supabase je vlastníkem obvykle postgres, který RLS obchází, takže view nad chráněnou tabulkou vrací řádky, které tabulka sama skrývá. Na mém lokálním stacku dostal veřejný klíč z tabulky 0 řádků a z obyčejného view nad ní všechny 3 ze 3; s security_invoker = true vrátilo view také 0. Na Postgresu 15 a novějším ho nastavte na každém view ve vystaveném schématu.
Můj bezplatný bezpečnostní checker pro Supabase má pravidlo „Pohledy, které obcházejí RLS“ a Security Advisor od Supabase hlásí totéž jako security_definer_view. Snadno se přehlédne, protože tabulka, ze které data unikají, vypadá chráněně.
Proč view v Supabase obchází RLS?
View je uložený dotaz. Ve výchozím stavu Postgres kontroluje oprávnění a RLS na tabulkách uvnitř něj jako vlastník view, ne jako ten, kdo ho volá. Průvodce RLS od Supabase to říká přímo: views ve výchozím stavu obcházejí RLS, protože se obvykle vytvářejí uživatelem postgres, a view nad chráněnou tabulkou vydá každý řádek, který její politiky měly zadržet.
Data API servíruje views stejně jako tabulky. Je-li view ve vystaveném schématu a anon nebo authenticated z něj smí vybírat, přečte ho veřejný klíč.
Co view vrátilo v mém testu?
Na lokálním stacku Supabase (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) jsem vytvořil tabulku orders se třemi fiktivními zákazníky, zapnul RLS bez politiky a nad ní view order_summary s grantem select pro anon. Pak jsem obojí zavolal s publishable key:
| Požadavek s publishable key | Výsledek |
|---|---|
GET /rest/v1/orders (RLS zapnuté, bez politiky) |
[] |
GET /rest/v1/order_summary (obyčejné view) |
všechny 3 řádky, se jmény a e-maily |
GET /rest/v1/order_summary po security_invoker = true |
[] |
Stejná tabulka, stejný klíč. Odpověď změnilo jen nastavení view.
Jak to opravit?
Na Postgresu 15 a novějším, který běží na každém aktuálním projektu Supabase, nechte view kontrolovat RLS jako volající:
alter view public.order_summary set (security_invoker = true);
U nového view ho uveďte v definici:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Volající pak potřebuje také select na podkladovou tabulku a o řádcích rozhodují politiky tabulky. Pokud view musí agregovat data, která volající po řádcích vidět nesmí, například veřejný počet, nechte ho mimo vystavené schéma a nabídněte ho přes funkci s úzkým výsledkem.
Seznam views v public, které stále běží jako jejich vlastník:
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);
Na mém stacku, s jedním obyčejným view a dvěma se security_invoker = true, vrátil jen to obyčejné.
Materializované views RLS nepodporují vůbec, proto je checker hlásí v API zvlášť, pod „Materializované pohledy nebo cizí tabulky v API“. Co dalšího obchází politiky, od secret key po funkce SECURITY DEFINER, je v mém checklistu zabezpečení aplikace z Lovable a jak dokázat, že samotné politiky fungují, ukazuje jak testuji Supabase RLS. Politika, která vyhledává týmy přes pomocnou funkci, bez rekurze, je v článku Supabase RLS policy: příklady otestované přes API. Funkce se SECURITY DEFINER potřebuje ještě jedno nastavení, pevný search_path; proč v mém testu search_path = public nestačilo, je v poznámce Supabase „function search path mutable“: co to znamená.