---
title: "NULL v PL/pgSQL IF NOT preskočil kontrolu pri 3683 úlohách"
description: "Vo funkcii SECURITY DEFINER je false OR NULL rovné NULL a IF NOT preskočí RAISE. Prečo je v RLS rovnaká kontrola bezpečná, oprava cez coalesce, ako to nájsť."
url: "https://madsalt.tech/sk/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "sk"
published: 2026-10-18
---

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

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](https://madsalt.tech/sk/work/audit-firm-time-tracking/), vo funkcii za tlačidlom „klonovať úlohu“.

## Ako kontrola vyzerala?

```sql
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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) aj pre [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); 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é.](https://madsalt.tech/charts/sk/plpgsql-if-not-null-fails-open.svg)

*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:

```sql
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](https://madsalt.tech/sk/blog/how-to-test-supabase-rls/), 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](https://madsalt.tech/sk/notes/supabase-rls-user-in-two-organizations/). Aplikácia bola evidencia času audítorskej firmy; jej ďalšie tiché chyby sú v článku [evidencia času pre audítorskú firmu](https://madsalt.tech/sk/blog/time-tracking-for-audit-firms/). [Kontrola zabezpečenia Supabase](https://madsalt.tech/sk/tools/supabase-security-check/) 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ý.
