TL;DR
Advarselen betyr at en funksjon mangler fast search_path, så hvert ukvalifiserte navn i den, som admins, slås opp slik den kallendes økt sier. I den lokale testen min returnerte en SECURITY DEFINER-funksjon is_admin() true for en ikke-admin etter at brukeren hadde opprettet en midlertidig tabell kalt admins, og å låse search_path = public stoppet det ikke. Det som virket: set search_path = '' med hver tabell skrevet som public.admins.
Supabases Security Advisor lister dette som lint 0011, «Function Search Path Mutable», nivå WARN, med detaljen «Function public.x has a role mutable search_path». Den flagger hver funksjon uten innstillingen, også SECURITY INVOKER-funksjoner, så i et prosjekt med mange hjelpere blir listen lang: ett åpen kildekode-prosjekt rettet 99 av dem i én enkelt migrering.
Hva betyr «role mutable search_path»?
search_path er listen over skjemaer Postgres søker i etter et navn uten skjema foran. En funksjon uten egen innstilling bruker den kallendes liste. Supabases lint-side anbefaler å sette den til en tom streng, som tvinger hver referanse i funksjonen til å være fullt kvalifisert.
For en SECURITY DEFINER-funksjon betyr det mest: den kjører med eierens rettigheter, vanligvis postgres, som omgår RLS. Hvis den kallende kan styre hvilken admins den leser, styrer vedkommende en sjekk som kjører som postgres.
Kan noen utnytte det i testen min?
På en lokal Postgres 17.10 laget jeg en tabell public.admins med én admin og fire versjoner av den samme funksjonen, select exists (select 1 from admins where user_id = uid), alle SECURITY DEFINER. Så logget jeg inn som en rolle med lave rettigheter som kan kjøre SQL og har standardretten til å opprette midlertidige tabeller, og opprettet pg_temp.admins med bruker A, som ikke er admin:
| Funksjonens innstilling | Lint-betingelse | is_admin(A) etter den midlertidige tabellen |
|---|---|---|
| ingen | flagget | true |
set search_path = public |
består | true |
set search_path = '', kroppen bruker public.admins |
består | false |
set search_path = public, pg_temp |
består | false |
Den andre linjen er den som overrasket meg. En Supabase-maintainer skrev i 2024 at en fast ikke-tom sti ikke medfører noen sikkerhetsrisiko, og linten godtar den. Men Postgres søker i det midlertidige skjemaet først med mindre pg_temp er nevnt i stien, slik dokumentasjonen om å skrive SECURITY DEFINER-funksjoner advarer om. Så med search_path = public kan en midlertidig tabell fortsatt skygge for et ukvalifisert navn.
Hvem kan faktisk gjøre dette på Supabase?
Ikke en besøkende gjennom appen din. Data API-et kan verken opprette midlertidige tabeller eller endre øktens search_path, så publishable key alene kommer ikke dit. Det krever en direkte databaseforbindelse: en annen tjeneste med egen innlogging, et BI- eller rapporteringsverktøy, en konsulents skrivebeskyttede rolle, en lekket tilkoblingsstreng. I de fleste små apper er risikoen lav; i ethvert prosjekt der andre personer eller verktøy enn deg har databaseinnlogginger, rett definer-funksjonene først.
Hvordan fikser du det?
Opprett funksjonen på nytt med tom sti og skjemanavn i kroppen:
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 er like viktig som stien: en definer-funksjon i public kan kalles gjennom Data API-et, så uten den kan hvilken som helst gjest spørre /rpc/is_admin om en gitt bruker er admin. Gi den bare tilbake til rollene som trenger den, eller hold slike hjelpere i et skjema API-et ikke eksponerer.
Ikke kjør bare alter function public.is_admin(uuid) set search_path = ''. Det får Advisor til å tie og ødelegger funksjonen: i kjøringen min feilet neste kall med 42P01: relation "admins" does not exist. Operatorer fra utvidelser trenger også skjemaet sitt, som operator(extensions.<=>) for pgvector.
Slik lister du det linten vil flagge i public, definer-funksjoner først:
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;
Koster løsningen noe?
Noen ganger. Enhver set-klausul hindrer Postgres i å bygge inn en enkel SQL-funksjon i spørringen som kaller den, som rapportert på GitHub. Jeg målte begge formene på 200 000 rader med en indeks på den filtrerte kolonnen (median av 7 kjøringer):
| SQL-funksjon, deretter filtrert på eier | Uten set |
set search_path = '' |
|---|---|---|
Filter utenfor: visible_items() where owner = ... |
0,053 ms, indeks brukt | 36,7 ms, full skanning |
Filter innenfor: items_of(owner) |
0,056 ms | 0,082 ms |
Hvis en funksjon som returnerer et sett spørres med filter utenfor, flytt filteret inn som parameter før du låser stien.
Sjekk prosjektet ditt
- Rett i Advisor først
SECURITY DEFINER-funksjonene på listen; invoker-funksjoner kjører uansett med den kallendes rettigheter. - Den gratis Supabase-sikkerhetssjekken viser Advisor-funn ved siden av sine egne regler, blant annet definer-funksjoner som kan kalles gjennom API-et og definer-funksjoner som sjekker
current_user. - Mer om hva definer-funksjoner kan hoppe over står i NULL i PL/pgSQL IF NOT hoppet over sjekken på 3683 oppgaver, og policyer som kaller slike hjelpere i Supabase RLS policy-eksempler, testet via API-et.