TL;DR
Et Postgres-view kjører med rettighetene til eieren sin, med mindre det er opprettet med security_invoker = true. I Supabase er eieren vanligvis postgres, som omgår RLS, så et view over en beskyttet tabell returnerer rader som selve tabellen skjuler. På den lokale stacken min fikk den offentlige nøkkelen 0 rader fra tabellen og alle 3 av 3 fra et vanlig view over den; med security_invoker = true returnerte viewet også 0. På Postgres 15 og nyere: sett det på hvert view i et eksponert skjema.
Den gratis Supabase-sikkerhetssjekken min har en regel som heter “Views som omgår RLS”, og Supabases Security Advisor melder det samme som security_definer_view. Det er lett å overse, fordi tabellen det lekker fra ser beskyttet ut.
Hvorfor omgår et Supabase-view RLS?
Et view er en lagret spørring. Som standard sjekker Postgres rettigheter og RLS på tabellene inni det som viewets eier, ikke som den som kaller det. Supabases RLS-guide sier det rett ut: views omgår RLS som standard fordi de vanligvis opprettes med brukeren postgres, og et view over en beskyttet tabell deler ut hver rad som policyene skulle holde tilbake.
Data API-et serverer views som tabeller. Hvis viewet ligger i et eksponert skjema og anon eller authenticated kan gjøre select på det, leser den offentlige nøkkelen det.
Hva returnerte viewet i testen min?
På en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) opprettet jeg en tabell orders med tre falske kunder, slo på RLS uten policy, og la et view order_summary over den med select-tilgang for anon. Så kalte jeg begge med publishable key:
| Forespørsel med publishable key | Resultat |
|---|---|
GET /rest/v1/orders (RLS på, ingen policy) |
[] |
GET /rest/v1/order_summary (vanlig view) |
alle 3 rader, med navn og e-poster |
GET /rest/v1/order_summary etter security_invoker = true |
[] |
Samme tabell, samme nøkkel. Bare innstillingen på viewet endret hva som kom tilbake.
Hvordan fikser du det?
På Postgres 15 og nyere, som alle dagens Supabase-prosjekter kjører, lar du viewet sjekke RLS som den som kaller:
alter view public.order_summary set (security_invoker = true);
For et nytt view legger du det i definisjonen:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Den som kaller trenger da også select på den underliggende tabellen, og policyene på tabellen avgjør radene. Hvis et view må aggregere data som den som kaller ikke kan se rad for rad, for eksempel et offentlig antall, hold det utenfor det eksponerte skjemaet og server det i stedet gjennom en funksjon med et smalt resultat.
Slik lister du views i public som fortsatt kjører som eieren sin:
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);
På stacken min, med ett vanlig view og to med security_invoker = true, returnerte den bare det vanlige.
Materialiserte views støtter ikke RLS i det hele tatt, så sjekken flagger dem i API-et separat, under “Materialiserte views eller eksterne tabeller i API-et”. Hva annet som går utenom policyene, fra secret key til SECURITY DEFINER-funksjoner, står i sikkerhetsjekklisten min for Lovable-apper, og hvordan du beviser selve policyene, står i hvordan du tester Supabase RLS. En policy som slår opp team gjennom en hjelpefunksjon, uten rekursjon, står i Supabase RLS policy-eksempler, testet via API-et. En security definer-funksjon trenger én innstilling til, en fast search_path; hvorfor search_path = public ikke holdt i testen min, står i Supabase Function Search Path Mutable: hva det betyr.