TL;DR
Postgres-näkymä ajetaan omistajansa oikeuksilla, ellei sitä luoda asetuksella security_invoker = true. Supabasessa omistaja on yleensä postgres, joka ohittaa RLS:n, joten suojatun taulun päälle tehty näkymä palauttaa rivejä, jotka taulu itse piilottaa. Paikallisella stackillani julkinen avain sai taulusta 0 riviä ja tavallisesta näkymästä kaikki 3 kolmesta; asetuksella security_invoker = true näkymäkin palautti 0. Postgres 15:ssä ja uudemmissa: aseta se jokaiselle julkaistun skeeman näkymälle.
Ilmaisessa Supabase-tietoturvatarkistuksessani on sääntö nimeltä “RLS:n ohittavat näkymät”, ja Supabasen Security Advisor raportoi saman asian nimellä security_definer_view. Se on helppo ohittaa, koska taulu, josta se vuotaa, näyttää suojatulta.
Miksi Supabase-näkymä ohittaa RLS:n?
Näkymä on tallennettu kysely. Oletuksena Postgres tarkistaa sen sisällä olevien taulujen oikeudet ja RLS:n näkymän omistajana, ei kutsujana. Supabasen RLS-opas sanoo sen suoraan: näkymät ohittavat RLS:n oletuksena, koska ne luodaan yleensä käyttäjällä postgres, ja suojatun taulun päälle tehty näkymä jakaa jokaisen rivin, jonka politiikkojen piti pidättää.
Data API tarjoaa näkymät kuin taulut. Jos näkymä on julkaistussa skeemassa ja anon tai authenticated voi tehdä siihen select-kyselyn, julkinen avain lukee sen.
Mitä näkymä palautti testissäni?
Paikallisella Supabase-stackilla (CLI 2.109.1, Postgres 17.6, PostgREST 14.14) loin taulun orders kolmella tekaistulla asiakkaalla, otin RLS:n käyttöön ilman politiikkaa ja loin sen päälle näkymän order_summary, jolla oli select-oikeus roolille anon. Sitten kutsuin molempia publishable keyllä:
| Pyyntö publishable keyllä | Tulos |
|---|---|
GET /rest/v1/orders (RLS päällä, ei politiikkaa) |
[] |
GET /rest/v1/order_summary (tavallinen näkymä) |
kaikki 3 riviä, nimineen ja sähköposteineen |
GET /rest/v1/order_summary asetuksen security_invoker = true jälkeen |
[] |
Sama taulu, sama avain. Vain näkymän asetus muutti sen, mitä palautui.
Miten se korjataan?
Postgres 15:ssä ja uudemmissa, joita kaikki nykyiset Supabase-projektit ajavat, anna näkymän tarkistaa RLS kutsujana:
alter view public.order_summary set (security_invoker = true);
Uudessa näkymässä asetus kirjoitetaan määritykseen:
create view public.order_summary
with (security_invoker = true)
as select customer, email, total from public.orders;
Kutsuja tarvitsee tällöin myös select-oikeuden alla olevaan tauluun, ja taulun politiikat ratkaisevat rivit. Jos näkymän täytyy koota dataa, jota kutsuja ei voi nähdä rivi riviltä, kuten julkinen lukumäärä, pidä se poissa julkaistusta skeemasta ja tarjoa se sen sijaan funktion kautta, jonka tulos on suppea.
Näin listaat public-skeeman näkymät, jotka ajetaan edelleen omistajana:
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);
Stackillani, jossa oli yksi tavallinen näkymä ja kaksi asetuksella security_invoker = true, se palautti vain tavallisen.
Materialisoidut näkymät eivät tue RLS:ää lainkaan, joten tarkistus liputtaa ne API:ssa erikseen säännöllä “Materialisoidut näkymät tai vierastaulut API:ssa”. Mitä muuta kiertää politiikat, secret keystä SECURITY DEFINER -funktioihin, löytyy Lovable-sovelluksen tietoturvatarkistuslistastani, ja se, miten politiikat itse todistetaan, selviää artikkelista Supabase RLS:n testaus. Politiikka, joka hakee tiimit apufunktion kautta ilman rekursiota, löytyy artikkelista Supabase RLS policy -esimerkit testattuna API:n kautta. SECURITY DEFINER -funktio tarvitsee vielä yhden asetuksen, kiinteän search_pathin; miksi search_path = public ei riittänyt testissäni, selviää muistiinpanosta Supabase “function search path mutable”: mitä se tarkoittaa.