TL;DR
Kontrola oprávnění uvnitř funkce SECURITY DEFINER zněla if not (has_perm or created_by = me or assignee_id = me) then raise. V červnu 2026 neměl žádný z 3683 úkolů v aplikaci tvůrce ani řešitele. false OR NULL OR NULL je NULL, NOT NULL je pořád NULL a IF přeskočí své tělo, když je podmínka NULL. Chyba se tedy nikdy nevyhodila a kterýkoli přihlášený uživatel mohl úkoly klonovat. Oprava je coalesce(..., false) kolem celé podmínky.
Stalo se to v evidenci času pro auditorskou firmu, ve funkci za tlačítkem „klonovat úkol“.
Jak kontrola vypadala?
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;
Čte se to jako „odmítni, pokud nesmíš úkoly vytvářet, tento jsi nevytvořil, nebo ho nevlastníš“. Těch 3683 úkolů se vytvořilo automaticky, takže created_by i assignee_id byly u všech NULL. U role s vypnutými všemi oprávněními byla podmínka NOT (false OR NULL OR NULL), tedy NULL, a raise se nikdy nespustil.
Proč je stejná podmínka v RLS bezpečná?
Row level security bere cokoli, co není true, jako odmítnutí, takže NULL znamená ne. IF v PL/pgSQL bere cokoli, co není true, jako „tělo přeskoč“, a tady bylo tělem právě odmítnutí. Dokumentace PostgreSQL uvádí obě poloviny, pro row security i pro IF; dohromady dělají z téhož výrazu na jednom místě zákaz a na druhém povolení.
Funkce SECURITY DEFINER navíc běží mimo RLS, takže platí jen její vlastní kontrola. Stačí jedna větev s NULL: ostatní mohou být všechny false.
Co to opravilo?
Obalte celou podmínku tak, aby se z NULL stalo odmítnutí, a klonování berte jako vytvoření úkolu, ne jako úpravu zdroje:
if not coalesce((select has_perm('tasks', 'create')), false) then
raise exception 'forbidden' using errcode = '42501';
end if;
Stalo se to znovu?
Ano, asi o tři měsíce později, ve třech dalších funkcích: archivace úkolu, změna jeho typu a kontrola, zda se na něj smí zapisovat čas. Čtení SQL je nezachytilo. Zachytilo je ověření RLS role po roli, protože vypíná každé oprávnění zvlášť a kontroluje, že akce je odmítnuta.
Jak to prohledat?
grep 'if not (' najde syntaxi, ne chybu. Co fungovalo:
- Jedním dotazem vypsat těla všech funkcí přes
pg_get_functiondef. - V každém
if not (...) thenaif a <> b thenodstranitcoalesce(...),exists(...)a(select ...). Holý řetězecornebo porovnání nad sloupcem, který může být null, je kandidát. - Hledat také
if not <variable> thena dohledat, kde se proměnná nastavuje._ok := has_perm(...) or created_by = me;ukrývá stejnou chybu na dvou řádcích. - Každý helper používaný jako
if not helper(...)zavolat třemi způsoby uvnitř transakce, která se vrací zpět: s neznámým id, s argumentemNULLa se skutečným uživatelem, jehož očekávaný řádek chybí. Helper, který v kterémkoli z nich vrátíNULL, potřebujecoalescev místě volání nebo uvnitř.
V té aplikaci sweep zkontroloval 14 helperů, všechny už byly bezpečné. Chyba se týkala jen funkce klonování a tří později nalezených. Související mezera u multi-tenant aplikací: co RLS přehlédne u uživatele ve dvou org. Aplikace byla evidence času auditorské firmy; její další tiché chyby jsou v článku evidence času pro auditorskou firmu. Kontrola zabezpečení Supabase řadí funkce SECURITY DEFINER mezi svých 46 pravidel, ale nepozná, zda IF uvnitř některé z nich selhává otevřeně, takže tento sweep zůstává ruční.