TL;DR
Varovanie znamená, že funkcia nemá pevný search_path, takže každé nekvalifikované meno vnútri nej, napríklad admins, sa hľadá tak, ako hovorí relácia volajúceho. V mojom lokálnom teste vrátila funkcia is_admin() so SECURITY DEFINER pre používateľa, ktorý nie je admin, hodnotu true, keď si vytvoril dočasnú tabuľku s menom admins, a ani pevné search_path = public to nezastavilo. Fungovalo set search_path = '' s každou tabuľkou zapísanou ako public.admins.
Security Advisor od Supabase to uvádza ako lint 0011, „Function Search Path Mutable“, úroveň WARN, s detailom „Function public.x has a role mutable search_path“. Označí každú funkciu bez tohto nastavenia, vrátane tých so SECURITY INVOKER, takže pri projekte s mnohými pomocnými funkciami je zoznam dlhý: jeden open-source projekt opravil 99 z nich jednou migráciou.
Čo znamená „role mutable search_path“?
search_path je zoznam schém, v ktorých Postgres hľadá meno bez schémy pred ním. Funkcia bez vlastného nastavenia použije zoznam volajúceho. Stránka lintu Supabase odporúča nastaviť ho na prázdny reťazec, čo vynúti plne kvalifikované každé odkazovanie vnútri funkcie.
Pri funkcii so SECURITY DEFINER na tom záleží najviac: beží s právami svojho vlastníka, zvyčajne postgres, ktorý RLS obchádza. Ak volajúci môže riadiť, ktorú tabuľku admins funkcia číta, riadi kontrolu, ktorá beží ako postgres.
Dá sa to v mojom teste zneužiť?
Na lokálnom Postgrese 17.10 som vytvoril tabuľku public.admins s jedným adminom a štyri verzie tej istej funkcie, select exists (select 1 from admins where user_id = uid), všetky so SECURITY DEFINER. Potom som sa prihlásil ako rola s nízkymi oprávneniami, ktorá smie spúšťať SQL a má predvolené právo vytvárať dočasné tabuľky, a vytvoril pg_temp.admins obsahujúcu používateľa A, ktorý admin nie je:
| Nastavenie funkcie | Podmienka lintu | is_admin(A) po dočasnej tabuľke |
|---|---|---|
| žiadne | označené | true |
set search_path = public |
prejde | true |
set search_path = '', telo používa public.admins |
prejde | false |
set search_path = public, pg_temp |
prejde | false |
Druhý riadok ma prekvapil. Správca Supabase v roku 2024 napísal, že pevná neprázdna cesta nenesie žiadne bezpečnostné riziko, a lint ju prijíma. Postgres však hľadá najprv v dočasnej schéme, ak v ceste nie je uvedené pg_temp, ako varujú jeho dokumenty o písaní funkcií SECURITY DEFINER. Takže search_path = public stále nechá dočasnú tabuľku zatieniť nekvalifikované meno.
Kto to na Supabase naozaj môže urobiť?
Nie návštevník cez vašu aplikáciu. Data API nevie vytvárať dočasné tabuľky ani meniť search_path relácie, takže samotný publishable key to nezvládne. Treba priame pripojenie k databáze: druhá služba s vlastným prihlásením, BI alebo reportovací nástroj, read-only rola dodávateľa, uniknutý connection string. Pri väčšine malých aplikácií je riziko nízke; pri každom projekte, kde prihlásenie do databázy držia iní ľudia alebo nástroje než vy, opravte najprv definer funkcie.
Ako to opraviť?
Vytvorte funkciu znova s prázdnou cestou a menami schém v tele:
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 je rovnako dôležité ako cesta: definer funkcia v public je volateľná cez Data API, takže bez neho sa ktorýkoľvek hosť môže cez /rpc/is_admin spýtať, či je daný používateľ admin. Vráťte ho len rolám, ktoré ho potrebujú, alebo držte takéto pomocné funkcie v schéme, ktorú API nevystavuje.
Nespúšťajte len alter function public.is_admin(uuid) set search_path = ''. Umlčí to Advisor a pokazí funkciu: v mojom behu ďalšie volanie zlyhalo s 42P01: relation "admins" does not exist. Operátory rozšírení tiež potrebujú svoju schému, napríklad operator(extensions.<=>) pre pgvector.
Zoznam toho, čo lint v public označí, s definer funkciami na začiatku:
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;
Stojí oprava niečo?
Niekedy. Akákoľvek klauzula set bráni Postgresu vložiť jednoduchú SQL funkciu do dopytu, ktorý ju volá, ako uvádza GitHub. Zmeral som oba tvary na 200 000 riadkoch s indexom na filtrovanom stĺpci (medián zo 7 behov):
| SQL funkcia, potom filtrovaná podľa vlastníka | Bez set |
set search_path = '' |
|---|---|---|
Filter vonku: visible_items() where owner = ... |
0,053 ms, index použitý | 36,7 ms, full scan |
Filter vnútri: items_of(owner) |
0,056 ms | 0,082 ms |
Ak sa SQL funkcia vracajúca množinu dopytuje s filtrom mimo nej, presuňte filter dovnútra ako parameter, skôr než pevne nastavíte cestu.
Skontrolujte svoj projekt
- V Advisore opravte najprv funkcie
SECURITY DEFINERna zozname; invoker funkcie aj tak bežia s právami volajúceho. - Bezplatný bezpečnostný checker pre Supabase ukazuje nálezy Advisora vedľa vlastných pravidiel, vrátane definer funkcií volateľných cez API a definer funkcií, ktoré kontrolujú
current_user. - Viac o tom, čo definer funkcie môžu preskočiť, je v poznámke NULL v PL/pgSQL IF NOT preskočil kontrolu pri 3683 úlohách a politiky, ktoré takéto pomocné funkcie volajú, sú v článku Supabase RLS policy: príklady otestované cez API.