Prejsť na obsah

NULL v PL/pgSQL IF NOT preskočil kontrolu pri 3683 úlohách

Postgres

Zobraziť ako MarkdownOtvoriť v ClaudeOtvoriť v ChatGPTOtvoriť v Perplexity

Otázka sa tiež skopíruje. Ak sa chat otvorí prázdny, vložte ju.

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.

Rovnaké NULL, dve opačné odpovede. has_perm je false, created_by a assignee_id sú NULL. V politike RLS: false OR NULL → NULL → Riadok odmietnutý; V IF NOT (...) THEN RAISE: false OR NULL → NOT NULL je NULL → RAISE preskočený → Klonovanie povolené.
Trojhodnotová logika podľa dokumentácie PostgreSQL 18, oddiely 5.9 (row security) a 41.6.4 (IF).

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:

  1. Jedným dotazom vypísať telá všetkých funkcií cez pg_get_functiondef.
  2. V každom if not (...) then a if a <> b then odstrániť coalesce(...), exists(...) a (select ...). Holý reťazec or alebo porovnanie nad stĺpcom, ktorý môže byť null, je kandidát.
  3. Hľadať aj if not <variable> then a dohľadať, kde sa premenná nastavuje. _ok := has_perm(...) or created_by = me; ukrýva rovnakú chybu na dvoch riadkoch.
  4. 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 argumentom NULL a so skutočným používateľom, ktorého očakávaný riadok chýba. Helper, ktorý v ktoromkoľvek z nich vráti NULL, potrebuje coalesce v 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ý.

© 2026 Denys Zaichenko. Pôvodný článok: https://madsalt.tech/sk/notes/plpgsql-if-not-null-fails-open/