TL;DR
Kontrola oprávnení vo vnútri funkcie SECURITY DEFINER znela if not (has_perm or created_by = me or assignee_id = me) then raise. V júni 2026 nemala žiadna z 3683 úloh v aplikácii tvorcu ani riešiteľa. false OR NULL OR NULL je NULL, NOT NULL je stále NULL a IF preskočí svoje telo, keď je podmienka NULL. Chyba sa teda nikdy nevyhodila a ktorýkoľvek prihlásený používateľ mohol úlohy klonovať. Oprava je coalesce(..., false) okolo celej podmienky.
Stalo sa to v sledovaní času pre audítorskú firmu, vo funkcii za tlačidlom „klonovať úlohu“.
Ako kontrola vyzerala?
if not ( has_perm('tasks', 'create')
or v_task.created_by = auth.uid()
or v_task.assignee_id = auth.uid() ) then
raise exception 'forbidden';
end if;
Číta sa to ako „odmietni, ak nesmieš úlohy vytvárať, túto si nevytvoril, alebo ju nevlastníš“. Tých 3683 úloh sa vytvorilo automaticky, takže created_by aj assignee_id boli pri všetkých NULL. Pri role s vypnutými všetkými oprávneniami bola podmienka NOT (false OR NULL OR NULL), teda NULL, a raise sa nikdy nespustil.
Prečo je rovnaká podmienka v RLS bezpečná?
Row level security berie všetko, čo nie je true, ako odmietnutie, takže NULL znamená nie. IF v PL/pgSQL berie všetko, čo nie je true, ako „telo preskoč“, a tu bolo telom práve odmietnutie. Dokumentácia PostgreSQL uvádza obe polovice, pre row security aj pre IF; dohromady robia z toho istého výrazu na jednom mieste zákaz a na druhom povolenie.
Funkcia SECURITY DEFINER navyše beží mimo RLS, takže platí iba jej vlastná kontrola. Stačí jedna vetva s NULL: ostatné môžu byť všetky false.
Čo to opravilo?
Obaľte celú podmienku tak, aby sa z NULL stalo odmietnutie, a klonovanie berte ako vytvorenie úlohy, nie ako úpravu zdroja:
if not coalesce((select has_perm('tasks', 'create')), false) then
raise exception 'forbidden' using errcode = '42501';
end if;
Stalo sa to znova?
Áno, asi o tri mesiace neskôr, v troch ďalších funkciách: archivácia úlohy, zmena jej typu a kontrola, či sa na ňu smie zapisovať čas. Čítanie SQL ich nezachytilo. Zachytilo ich overenie RLS rola po role, pretože vypína každé oprávnenie zvlášť a kontroluje, že akcia je odmietnutá.
Ako to prehľadať?
grep 'if not (' nájde syntax, nie chybu. Čo fungovalo:
- Jedným dotazom vypísať telá všetkých funkcií cez
pg_get_functiondef. - V každom
if not (...) thenaif a <> b thenodstrániťcoalesce(...),exists(...)a(select ...). Holý reťazecoralebo porovnanie nad stĺpcom, ktorý môže byť null, je kandidát. - Hľadať aj
if not <variable> thena dohľadať, kde sa premenná nastavuje._ok := has_perm(...) or created_by = me;ukrýva rovnakú chybu na dvoch riadkoch. - Každý helper používaný ako
if not helper(...)zavolať tromi spôsobmi vo vnútri transakcie, ktorá sa vráti späť: s neznámym id, s argumentomNULLa so skutočným používateľom, ktorého očakávaný riadok chýba. Helper, ktorý v ktoromkoľvek z nich vrátiNULL, potrebujecoalescev mieste volania alebo vo vnútri.
V tej aplikácii sweep skontroloval 14 helperov, všetky už boli bezpečné. Chyba sa týkala len funkcie klonovania a troch neskôr nájdených. Súvisiaca medzera pri multi-tenant aplikáciách: čo RLS prehliadne u používateľa v dvoch org. Aplikácia bola evidencia času audítorskej firmy; jej ďalšie tiché chyby sú v článku evidencia času pre audítorskú firmu. Kontrola zabezpečenia Supabase uvádza funkcie SECURITY DEFINER medzi svojimi 46 pravidlami, ale nevie povedať, či IF v nich nepustí kontrolu (fails open), takže tento sweep zostáva ručný.