TL;DR
Varoitus tarkoittaa, että funktiolla ei ole kiinteää search_path-asetusta, joten jokainen sen sisällä oleva ilman skeemaa kirjoitettu nimi, kuten admins, haetaan niin kuin kutsujan istunto sanoo. Paikallisessa testissäni SECURITY DEFINER -funktio is_admin() palautti true käyttäjälle, joka ei ole admin, kun tämä oli luonut väliaikaisen taulun nimeltä admins, eikä kiinteä search_path = public pysäyttänyt sitä. Toimi set search_path = '', kun jokainen taulu kirjoitetaan muodossa public.admins.
Supabasen Security Advisor listaa tämän lintiksi 0011, “Function Search Path Mutable”, taso WARN, tarkenteella “Function public.x has a role mutable search_path”. Se liputtaa jokaisen funktion, jolla asetusta ei ole, myös SECURITY INVOKER -funktiot, joten projektissa, jossa on paljon apufunktioita, lista venyy pitkäksi: yksi avoimen lähdekoodin projekti korjasi niistä 99 yhdellä migraatiolla.
Mitä “role mutable search_path” tarkoittaa?
search_path on lista skeemoista, joista Postgres etsii nimeä, jonka edessä ei ole skeemaa. Funktio, jolla ei ole omaa asetusta, käyttää kutsujan listaa. Supabasen lint-sivu suosittelee asettamaan sen tyhjäksi merkkijonoksi, mikä pakottaa kirjoittamaan jokaisen funktion sisällä olevan viittauksen täydellä skeemanimellä.
SECURITY DEFINER -funktiolle tällä on eniten merkitystä: se ajetaan omistajansa oikeuksilla, yleensä postgres, joka ohittaa RLS:n. Jos kutsuja voi ohjata, mitä admins-taulua funktio lukee, kutsuja ohjaa tarkistusta, joka ajetaan käyttäjänä postgres.
Voiko tätä hyödyntää testissäni?
Paikallisessa Postgres 17.10:ssä tein taulun public.admins, jossa oli yksi admin, ja neljä versiota samasta funktiosta, select exists (select 1 from admins where user_id = uid), kaikki SECURITY DEFINER. Sitten kirjauduin sisään vähäoikeuksisena roolina, joka voi ajaa SQL:ää ja jolla on oletuksena oikeus luoda väliaikaisia tauluja, ja loin taulun pg_temp.admins, jossa oli käyttäjä A, joka ei ole admin:
| Funktion asetus | Lint-ehto | is_admin(A) väliaikaisen taulun jälkeen |
|---|---|---|
| ei mitään | liputettu | true |
set search_path = public |
läpäisee | true |
set search_path = '', rungossa public.admins |
läpäisee | false |
set search_path = public, pg_temp |
läpäisee | false |
Toinen rivi yllätti minut. Supabasen ylläpitäjä kirjoitti vuonna 2024, ettei kiinteä, ei-tyhjä polku aiheuta tietoturvariskiä, ja lint hyväksyy sen. Postgres kuitenkin hakee ensin väliaikaisesta skeemasta, ellei polussa mainita pg_temp, kuten sen SECURITY DEFINER -funktioiden kirjoittamista käsittelevä dokumentaatio varoittaa. Siis search_path = public antaa väliaikaisen taulun silti peittää skeematon nimi.
Kuka tämän voi oikeasti tehdä Supabasessa?
Ei sivuston kävijä sovelluksesi kautta. Data API ei voi luoda väliaikaisia tauluja eikä muuttaa istunnon search_path-asetusta, joten pelkkä publishable key ei riitä. Tarvitaan suora tietokantayhteys: toinen palvelu omalla tunnuksellaan, BI- tai raportointityökalu, ulkopuolisen tekijän vain luku -rooli tai vuotanut connection string. Useimmissa pienissä sovelluksissa riski on pieni; projektissa, jossa tietokantatunnuksia on muillakin ihmisillä tai työkaluilla kuin sinulla, korjaa definer-funktiot ensin.
Miten se korjataan?
Luo funktio uudelleen tyhjällä polulla ja skeemanimillä rungossa:
create or replace function public.is_admin(uid uuid)
returns boolean
language sql stable security definer
set search_path = ''
as $$ select exists (select 1 from public.admins where user_id = uid) $$;
revoke execute on function public.is_admin(uuid) from public, anon;
revoke on yhtä tärkeä kuin polku: public-skeeman definer-funktio on kutsuttavissa Data API:n kautta, joten ilman sitä kuka tahansa vieras voi kysyä osoitteesta /rpc/is_admin, onko tietty käyttäjä admin. Anna oikeus takaisin vain rooleille, jotka sitä tarvitsevat, tai pidä tällaiset apufunktiot skeemassa, jota API ei julkaise.
Älä aja pelkkää alter function public.is_admin(uuid) set search_path = ''. Se vaientaa Advisorin ja rikkoo funktion: ajossani seuraava kutsu epäonnistui virheeseen 42P01: relation "admins" does not exist. Laajennusten operaattorit tarvitsevat myös skeemansa, esimerkiksi operator(extensions.<=>) pgvectorille.
Lista siitä, mitä lint liputtaa public-skeemassa, definer-funktiot ensin:
select p.proname, p.prosecdef as security_definer
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public'
and not exists (select 1 from unnest(coalesce(p.proconfig, '{}')) c
where c like 'search_path=%')
order by p.prosecdef desc, p.proname;
Maksaako korjaus jotain?
Joskus. Mikä tahansa set-lauseke estää Postgresia upottamasta tavallista SQL-funktiota sitä kutsuvaan kyselyyn, kuten GitHubissa on raportoitu. Mittasin molemmat muodot 200 000 rivillä, suodatettavassa sarakkeessa indeksi (mediaani 7 ajosta):
| SQL-funktio, sitten suodatus omistajan mukaan | Ilman set |
set search_path = '' |
|---|---|---|
Suodatin ulkopuolella: visible_items() where owner = ... |
0,053 ms, indeksi käytössä | 36,7 ms, full scan |
Suodatin sisällä: items_of(owner) |
0,056 ms | 0,082 ms |
Jos joukon palauttavaa SQL-funktiota kysytään suodattimella sen ulkopuolella, siirrä suodatin parametriksi funktion sisään ennen kuin lukitset polun.
Tarkista projektisi
- Korjaa Advisorin listalta ensin
SECURITY DEFINER-funktiot; invoker-funktiot ajetaan joka tapauksessa kutsujan oikeuksilla. - Ilmainen Supabase-tietoturvatarkistus näyttää Advisorin havainnot omien sääntöjensä vieressä, mukaan lukien API:n kautta kutsuttavat definer-funktiot ja definer-funktiot, jotka tarkistavat
current_userin. - Lisää siitä, mitä definer-funktiot voivat ohittaa, on muistiinpanossa PL/pgSQL IF NOT ja NULL ohitti tarkistuksen 3683 tehtävällä, ja tällaisia apufunktioita kutsuvat politiikat löytyvät artikkelista Supabase RLS policy -esimerkit testattuna API:n kautta.