Přejít na obsah

NULL v PL/pgSQL IF NOT přeskočil kontrolu u 3683 úkolů

Postgres

Zobrazit jako MarkdownOtevřít v ClaudeOtevřít v ChatGPTOtevřít v Perplexity

Otázka se zkopíruje také. Pokud se chat otevře prázdný, vložte ji.

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í.

Stejné NULL, dvě opačné odpovědi. has_perm je false, created_by a assignee_id jsou NULL. V politice RLS: false OR NULL → NULL → Řádek odmítnut; V IF NOT (...) THEN RAISE: false OR NULL → NOT NULL je NULL → RAISE přeskočen → Klonování povoleno.
Třívalentní logika podle dokumentace PostgreSQL 18, oddíly 5.9 (row security) a 41.6.4 (IF).

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:

  1. Jedním dotazem vypsat těla všech funkcí přes pg_get_functiondef.
  2. V každém if not (...) then a if a <> b then odstranit coalesce(...), exists(...) a (select ...). Holý řetězec or nebo porovnání nad sloupcem, který může být null, je kandidát.
  3. Hledat také if not <variable> then a dohledat, kde se proměnná nastavuje. _ok := has_perm(...) or created_by = me; ukrývá stejnou chybu na dvou řádcích.
  4. 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 argumentem NULL a se skutečným uživatelem, jehož očekávaný řádek chybí. Helper, který v kterémkoli z nich vrátí NULL, potřebuje coalesce v 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í.

© 2026 Denys Zaichenko. Původní článek: https://madsalt.tech/cs/notes/plpgsql-if-not-null-fails-open/