Kort sagt
En Postgres-vy körs med sin ägares rättigheter om den inte skapas med security_invoker = true. I Supabase är ägaren oftast postgres, som kringgår RLS, så en vy över en skyddad tabell returnerar rader som tabellen själv döljer. På min lokala stack fick den publika nyckeln 0 rader från tabellen och alla 3 av 3 från en vanlig vy över den; med security_invoker = true returnerade vyn också 0. På Postgres 15 och senare: sätt det på varje vy i ett exponerat schema.
Min gratis Supabase-säkerhetskontroll har en regel som heter “Vyer som kringgår RLS”, och Supabases Security Advisor rapporterar samma sak som security_definer_view. Det är lätt att missa, eftersom tabellen den läcker från ser skyddad ut.
Varför kringgår en Supabase-vy RLS?
En vy är en lagrad fråga. Som standard kontrollerar Postgres behörigheter och RLS på tabellerna i den som vyns ägare, inte som den som anropar den. Supabases RLS-guide säger det rakt ut: vyer kringgår RLS som standard eftersom de oftast skapas med användaren postgres, och en vy över en skyddad tabell delar ut varje rad som dess policyer var avsedda att hålla tillbaka.
Data API:et serverar vyer som tabeller. Om vyn ligger i ett exponerat schema och anon eller authenticated kan göra select på den, läser den publika nyckeln den.
Vad returnerade vyn i mitt test?
På en lokal Supabase-stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) skapade jag en tabell orders med tre falska kunder, slog på RLS utan policy och la en vy order_summary över den med select-behörighet för anon. Sedan anropade jag båda med publishable key:
| Förfrågan med publishable key | Resultat |
|---|---|
GET /rest/v1/orders (RLS på, ingen policy) |
[] |
GET /rest/v1/order_summary (vanlig vy) |
alla 3 rader, med namn och e-post |
GET /rest/v1/order_summary efter security_invoker = true |
[] |
Samma tabell, samma nyckel. Bara vyns inställning ändrade vad som kom tillbaka.
Hur fixar du det?
På Postgres 15 och senare, som alla nuvarande Supabase-projekt kör, låter du vyn kontrollera RLS som den som anropar:
alter view public.order_summary set (security_invoker = true);
För en ny vy lägger du det i definitionen:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Den som anropar behöver då också select på den underliggande tabellen, och tabellens policyer avgör raderna. Om en vy måste aggregera data som den som anropar inte kan se rad för rad, till exempel ett publikt antal, håll den utanför det exponerade schemat och servera den i stället via en funktion med ett snävt resultat.
Så listar du vyerna i public som fortfarande körs som sin ägare:
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 en vanlig vy och två med security_invoker = true, returnerade den bara den vanliga.
Materialiserade vyer stöder inte RLS alls, så kontrollen flaggar dem i API:t separat, under “Materialiserade vyer eller främmande tabeller i API:t”. Vad mer som går förbi policyerna, från secret key till SECURITY DEFINER-funktioner, finns i min säkerhetschecklista för Lovable-appar, och hur du bevisar själva policyerna finns i hur du testar Supabase RLS. En policy som slår upp team via en hjälpfunktion, utan rekursion, finns i Supabase RLS policy-exempel, testade via API:t. En security definer-funktion behöver ytterligare en inställning, en fast search_path; varför search_path = public inte räckte i mitt test finns i Supabase Function Search Path Mutable: vad det betyder.