TL;DR
Advarslen betyder, at en funktion ikke har en fast search_path, så hvert ukvalificeret navn i den, som admins, slås op, som den kaldendes session angiver. I min lokale test returnerede en SECURITY DEFINER-funktion is_admin() true for en ikke-admin, efter at brugeren havde oprettet en midlertidig tabel kaldet admins, og at låse search_path = public stoppede det ikke. Det, der virkede: set search_path = '' med hver tabel skrevet som public.admins.
Supabases Security Advisor angiver dette som lint 0011, “Function Search Path Mutable”, niveau WARN, med detaljen “Function public.x has a role mutable search_path”. Den flager hver funktion uden indstillingen, også SECURITY INVOKER-funktioner, så på et projekt med mange hjælpere bliver listen lang: et open source-projekt rettede 99 af dem i én enkelt migration.
Hvad betyder “role mutable search_path”?
search_path er listen over skemaer, Postgres leder i efter et navn uden skema foran. En funktion uden egen indstilling bruger den kaldendes liste. Supabases lint-side anbefaler at sætte den til en tom streng, hvilket tvinger hver reference i funktionen til at være fuldt kvalificeret.
For en SECURITY DEFINER-funktion betyder det mest: den kører med sin ejers rettigheder, normalt postgres, som omgår RLS. Hvis den kaldende kan styre, hvilken admins den læser, styrer vedkommende et tjek, der kører som postgres.
Kan nogen udnytte det i min test?
På en lokal Postgres 17.10 lavede jeg en tabel public.admins med én admin og fire versioner af den samme funktion, select exists (select 1 from admins where user_id = uid), alle SECURITY DEFINER. Derefter loggede jeg ind som en rolle med lave rettigheder, der kan køre SQL og har standardretten til at oprette midlertidige tabeller, og oprettede pg_temp.admins med bruger A, som ikke er admin:
| Funktionens indstilling | Lint-betingelse | is_admin(A) efter den midlertidige tabel |
|---|---|---|
| ingen | flagget | true |
set search_path = public |
består | true |
set search_path = '', kroppen bruger public.admins |
består | false |
set search_path = public, pg_temp |
består | false |
Den anden linje er den, der overraskede mig. En Supabase-maintainer skrev i 2024, at en fast ikke-tom sti ikke indebærer nogen sikkerhedsrisiko, og linten accepterer den. Men Postgres leder i det midlertidige skema først, medmindre pg_temp er nævnt i stien, som dens dokumentation om at skrive SECURITY DEFINER-funktioner advarer om. Så med search_path = public kan en midlertidig tabel stadig skygge for et ukvalificeret navn.
Hvem kan faktisk gøre det på Supabase?
Ikke en besøgende gennem din app. Data API’et kan hverken oprette midlertidige tabeller eller ændre sessionens search_path, så publishable key alene når ikke dertil. Det kræver en direkte databaseforbindelse: en anden tjeneste med eget login, et BI- eller rapporteringsværktøj, en konsulents skrivebeskyttede rolle, en lækket forbindelsesstreng. På de fleste små apps er risikoen lav; på ethvert projekt, hvor andre personer eller værktøjer end dig har databaselogins, så ret definer-funktionerne først.
Hvordan retter du det?
Opret funktionen igen med en tom sti og skemanavne 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 betyder lige så meget som stien: en definer-funktion i public kan kaldes gennem Data API’et, så uden den kan enhver gæst spørge /rpc/is_admin, om en given bruger er admin. Giv den kun tilbage til de roller, der har brug for den, eller hold hjælpere som denne i et skema, API’et ikke eksponerer.
Kør ikke kun alter function public.is_admin(uuid) set search_path = ''. Det får Advisor til at tie og ødelægger funktionen: i min kørsel fejlede næste kald med 42P01: relation "admins" does not exist. Operatorer fra udvidelser har også brug for deres skema, som operator(extensions.<=>) til pgvector.
Sådan lister du det, linten vil flage i public, med definer-funktioner 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 rettelsen noget?
Nogle gange. Enhver set-klausul forhindrer Postgres i at indlejre en simpel SQL-funktion i den forespørgsel, der kalder den, som rapporteret på GitHub. Jeg målte begge former på 200.000 rækker med et indeks på den filtrerede kolonne (median af 7 kørsler):
| SQL-funktion, derefter filtreret på ejer | Uden set |
set search_path = '' |
|---|---|---|
Filter udenfor: visible_items() where owner = ... |
0,053 ms, indeks brugt | 36,7 ms, fuld scanning |
Filter indeni: items_of(owner) |
0,056 ms | 0,082 ms |
Hvis en funktion, der returnerer et sæt, forespørges med et filter udenfor, så flyt filteret ind som parameter, før du låser stien.
Tjek dit projekt
- Ret i Advisor først
SECURITY DEFINER-funktionerne på listen; invoker-funktioner kører alligevel med den kaldendes rettigheder. - Det gratis Supabase-sikkerhedstjek viser Advisor-fund ved siden af sine egne regler, herunder definer-funktioner, der kan kaldes gennem API’et, og definer-funktioner, der tjekker
current_user. - Mere om, hvad definer-funktioner kan springe over, står i En NULL i PL/pgSQL IF NOT sprang tjek over for 3683 opgaver, og policies, der kalder sådanne hjælpere, i Supabase RLS policy-eksempler, hver testet via API.