TL;DR
Et view i Postgres kører med sin ejers rettigheder, medmindre det er oprettet med security_invoker = true. I Supabase er ejeren som regel postgres, som omgår RLS, så et view over en beskyttet tabel returnerer rækker, som tabellen selv skjuler. På min lokale stack fik den offentlige nøgle 0 rækker fra tabellen og alle 3 af 3 fra et almindeligt view over den; med security_invoker = true returnerede viewet også 0. På Postgres 15 og nyere bør du sætte den på hvert view i et eksponeret schema.
Mit gratis Supabase-sikkerhedstjek har en regel, der hedder “Views, der omgår RLS”, og Supabases Security Advisor melder det samme som security_definer_view. Det er let at overse, fordi den tabel, der lækker, ser beskyttet ud.
Hvorfor omgår et Supabase-view RLS?
Et view er en gemt forespørgsel. Som standard tjekker Postgres rettigheder og RLS på tabellerne i det som viewets ejer, ikke som den, der kalder det. Supabases RLS-guide siger det direkte: views omgår RLS som standard, fordi de som regel oprettes med brugeren postgres, og et view over en beskyttet tabel udleverer hver række, som tabellens policies skulle have holdt tilbage.
Data API’et leverer views som tabeller. Hvis viewet ligger i et eksponeret schema, og anon eller authenticated kan vælge fra det, læser den offentlige nøgle det.
Hvad returnerede viewet i min test?
På en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) oprettede jeg en tabel orders med tre falske kunder, aktiverede RLS uden policy og oprettede et view order_summary over den med en select-rettighed til anon. Derefter kaldte jeg begge med publishable key:
| Anmodning med publishable key | Resultat |
|---|---|
GET /rest/v1/orders (RLS slået til, ingen policy) |
[] |
GET /rest/v1/order_summary (almindeligt view) |
alle 3 rækker, med navne og e-mails |
GET /rest/v1/order_summary efter security_invoker = true |
[] |
Samme tabel, samme nøgle. Kun viewets indstilling ændrede, hvad der kom tilbage.
Hvordan retter du det?
På Postgres 15 og nyere, som alle nuværende Supabase-projekter kører, kan du få viewet til at tjekke RLS som den, der kalder:
alter view public.order_summary set (security_invoker = true);
Til et nyt view skriver du det i definitionen:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Den, der kalder, skal så også have select på den underliggende tabel, og tabellens policies afgør rækkerne. Hvis et view skal aggregere data, som den, der kalder, ikke må se række for række, for eksempel et offentligt antal, så hold det uden for det eksponerede schema og lever det gennem en funktion med et snævert resultat.
Sådan lister du de views i public, der stadig kører som deres ejer:
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å min stack, med ét almindeligt view og to med security_invoker = true, returnerede den kun det almindelige.
Materialized views understøtter slet ikke RLS, så tjekket flagger dem i API’et for sig, under “Materialized views eller foreign tables i API’et”. Hvad der ellers går uden om policies, fra secret key til SECURITY DEFINER-funktioner, står i min sikkerhedstjekliste til Lovable-apps, og hvordan du beviser selve policies, står i sådan tester jeg Supabase RLS. En policy, der slår teams op gennem en hjælpefunktion uden rekursion, står i Supabase RLS policy-eksempler, hver testet via API. En security definer-funktion har brug for en indstilling mere, en fast search_path; hvorfor search_path = public ikke var nok i min test, står i Supabase Function Search Path Mutable: hvad det betyder.