TL;DR
View v Postgrese beží s právami svojho vlastníka, ak nie je vytvorené so security_invoker = true. V Supabase je vlastníkom zvyčajne postgres, ktorý RLS obchádza, takže view nad chránenou tabuľkou vracia riadky, ktoré tabuľka sama skrýva. Na mojom lokálnom stacku dostal verejný kľúč z tabuľky 0 riadkov a z obyčajného view nad ňou všetky 3 z 3; so security_invoker = true vrátilo view tiež 0. Na Postgrese 15 a novšom ho nastavte na každom view vo vystavenej schéme.
Môj bezplatný bezpečnostný checker pre Supabase má pravidlo „Pohľady, ktoré obchádzajú RLS“ a Security Advisor od Supabase hlási to isté ako security_definer_view. Ľahko sa prehliadne, pretože tabuľka, z ktorej dáta unikajú, vyzerá chránene.
Prečo view v Supabase obchádza RLS?
View je uložený dotaz. V predvolenom stave Postgres kontroluje oprávnenia a RLS na tabuľkách vo vnútri ako vlastník view, nie ako ten, kto ho volá. Sprievodca RLS od Supabase to hovorí priamo: views v predvolenom stave obchádzajú RLS, pretože sa zvyčajne vytvárajú používateľom postgres, a view nad chránenou tabuľkou vydá každý riadok, ktorý jej politiky mali zadržať.
Data API servíruje views rovnako ako tabuľky. Ak je view vo vystavenej schéme a anon alebo authenticated z neho smie vyberať, prečíta ho verejný kľúč.
Čo view vrátilo v mojom teste?
Na lokálnom stacku Supabase (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) som vytvoril tabuľku orders s tromi fiktívnymi zákazníkmi, zapol RLS bez politiky a nad ňou view order_summary s grantom select pre anon. Potom som oboje zavolal s publishable key:
| Požiadavka s publishable key | Výsledok |
|---|---|
GET /rest/v1/orders (RLS zapnuté, bez politiky) |
[] |
GET /rest/v1/order_summary (obyčajné view) |
všetky 3 riadky, s menami a e-mailami |
GET /rest/v1/order_summary po security_invoker = true |
[] |
Rovnaká tabuľka, rovnaký kľúč. Odpoveď zmenilo iba nastavenie view.
Ako to opraviť?
Na Postgrese 15 a novšom, ktorý beží na každom aktuálnom projekte Supabase, nechajte view kontrolovať RLS ako volajúci:
alter view public.order_summary set (security_invoker = true);
Pri novom view ho uveďte v definícii:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Volajúci potom potrebuje aj select na podkladovú tabuľku a o riadkoch rozhodujú politiky tabuľky. Ak view musí agregovať dáta, ktoré volajúci po riadkoch vidieť nesmie, napríklad verejný počet, nechajte ho mimo vystavenej schémy a ponúknite ho cez funkciu s úzkym výsledkom.
Zoznam views v public, ktoré stále bežia ako ich 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 mojom stacku, s jedným obyčajným view a dvoma so security_invoker = true, vrátil iba to obyčajné.
Materializované views RLS nepodporujú vôbec, preto ich checker hlási v API osobitne, pod „Materializované pohľady alebo cudzie tabuľky v API“. Čo ďalšie obchádza politiky, od secret key po funkcie SECURITY DEFINER, je v mojom checkliste bezpečnosti aplikácie z Lovable a ako dokázať, že samotné politiky fungujú, ukazuje ako testujem Supabase RLS. Politika, ktorá vyhľadáva tímy cez pomocnú funkciu, bez rekurzie, je v článku Supabase RLS policy: príklady otestované cez API. Funkcia so SECURITY DEFINER potrebuje ešte jedno nastavenie, pevný search_path; prečo v mojom teste search_path = public nestačilo, je v poznámke Supabase „function search path mutable“: čo to znamená.