TL;DR
Varování znamená, že funkce nemá pevný search_path, takže každé nekvalifikované jméno uvnitř ní, například admins, se hledá tak, jak říká session volajícího. V mém lokálním testu vrátila funkce is_admin() s SECURITY DEFINER pro uživatele, který není admin, hodnotu true, poté co si vytvořil dočasnou tabulku jménem admins, a ani pevné search_path = public to nezastavilo. Fungovalo set search_path = '' s každou tabulkou zapsanou jako public.admins.
Security Advisor od Supabase to uvádí jako lint 0011, „Function Search Path Mutable“, úroveň WARN, s detailem „Function public.x has a role mutable search_path“. Označí každou funkci bez tohoto nastavení, včetně těch se SECURITY INVOKER, takže u projektu s mnoha pomocnými funkcemi je seznam dlouhý: jeden open-source projekt opravil 99 z nich jednou migrací.
Co znamená „role mutable search_path“?
search_path je seznam schémat, ve kterých Postgres hledá jméno bez schématu před ním. Funkce bez vlastního nastavení použije seznam volajícího. Stránka lintu Supabase doporučuje nastavit ho na prázdný řetězec, což vynutí plně kvalifikované každé odkazování uvnitř funkce.
U funkce se SECURITY DEFINER to záleží nejvíc: běží s právy svého vlastníka, obvykle postgres, který RLS obchází. Pokud volající může řídit, kterou tabulku admins funkce čte, řídí kontrolu, která běží jako postgres.
Dá se to v mém testu zneužít?
Na lokálním Postgresu 17.10 jsem vytvořil tabulku public.admins s jedním adminem a čtyři verze téže funkce, select exists (select 1 from admins where user_id = uid), všechny se SECURITY DEFINER. Pak jsem se přihlásil jako role s nízkými oprávněními, která smí spouštět SQL a má výchozí právo vytvářet dočasné tabulky, a vytvořil pg_temp.admins obsahující uživatele A, který admin není:
| Nastavení funkce | Podmínka lintu | is_admin(A) po dočasné tabulce |
|---|---|---|
| žádné | označeno | true |
set search_path = public |
projde | true |
set search_path = '', tělo používá public.admins |
projde | false |
set search_path = public, pg_temp |
projde | false |
Druhý řádek mě překvapil. Správce Supabase v roce 2024 napsal, že pevná neprázdná cesta nenese žádné bezpečnostní riziko, a lint ji přijímá. Postgres ale hledá nejdřív v dočasném schématu, pokud v cestě není uvedeno pg_temp, jak varují jeho dokumenty o psaní funkcí SECURITY DEFINER. Takže search_path = public stále nechá dočasnou tabulku zastínit nekvalifikované jméno.
Kdo to na Supabase opravdu může udělat?
Ne návštěvník přes vaši aplikaci. Data API neumí vytvářet dočasné tabulky ani měnit search_path session, takže samotný publishable key to nezvládne. Je potřeba přímé připojení k databázi: druhá služba s vlastním přihlášením, BI nebo reportovací nástroj, read-only role dodavatele, uniklý connection string. U většiny malých aplikací je riziko nízké; u každého projektu, kde přihlášení k databázi drží jiní lidé nebo nástroje než vy, opravte nejdřív definer funkce.
Jak to opravit?
Vytvořte funkci znovu s prázdnou cestou a jmény schémat v těle:
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 stejně důležité jako cesta: definer funkce v public je volatelná přes Data API, takže bez něj se kterýkoli host může přes /rpc/is_admin zeptat, zda je daný uživatel admin. Vraťte ho jen rolím, které ho potřebují, nebo držte takové pomocné funkce ve schématu, které API nevystavuje.
Nespouštějte jen alter function public.is_admin(uuid) set search_path = ''. Umlčí to Advisor a rozbije funkci: v mém běhu další volání selhalo s 42P01: relation "admins" does not exist. Operátory rozšíření také potřebují své schéma, například operator(extensions.<=>) pro pgvector.
Seznam toho, co lint v public označí, s definer funkcemi první:
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 něco?
Někdy. Jakákoli klauzule set brání Postgresu vložit prostou SQL funkci do dotazu, který ji volá, jak uvádí GitHub. Změřil jsem oba tvary na 200 000 řádcích s indexem na filtrovaném sloupci (medián ze 7 běhů):
| SQL funkce, pak filtrovaná podle vlastníka | Bez set |
set search_path = '' |
|---|---|---|
Filtr venku: visible_items() where owner = ... |
0,053 ms, index použit | 36,7 ms, full scan |
Filtr uvnitř: items_of(owner) |
0,056 ms | 0,082 ms |
Pokud se funkce SQL vracející množinu dotazuje s filtrem mimo ni, přesuňte filtr dovnitř jako parametr, než pevně nastavíte cestu.
Zkontrolujte svůj projekt
- V Advisoru opravte nejdřív funkce
SECURITY DEFINERna seznamu; invoker funkce stejně běží s právy volajícího. - Bezplatný bezpečnostní checker pro Supabase ukazuje nálezy Advisoru vedle vlastních pravidel, včetně definer funkcí volatelných přes API a definer funkcí, které kontrolují
current_user. - Víc o tom, co definer funkce mohou přeskočit, je v poznámce NULL v PL/pgSQL IF NOT přeskočil kontrolu u 3683 úkolů a politiky, které takové pomocné funkce volají, jsou v článku Supabase RLS policy: příklady otestované přes API.