---
title: "NULL v PL/pgSQL IF NOT přeskočil kontrolu u 3683 úkolů"
description: "Ve funkci SECURITY DEFINER je false OR NULL rovno NULL a IF NOT přeskočí RAISE. Proč je v RLS stejná kontrola bezpečná, oprava přes coalesce, jak to najít."
url: "https://madsalt.tech/cs/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "cs"
published: 2026-10-18
---

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

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](https://madsalt.tech/cs/work/audit-firm-time-tracking/), ve funkci za tlačítkem „klonovat úkol“.

## Jak kontrola vypadala?

```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;
```

Č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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) i pro [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); 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.](https://madsalt.tech/charts/cs/plpgsql-if-not-null-fails-open.svg)

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

```sql
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](https://madsalt.tech/cs/blog/how-to-test-supabase-rls/), 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](https://madsalt.tech/cs/notes/supabase-rls-user-in-two-organizations/). Aplikace byla evidence času auditorské firmy; její další tiché chyby jsou v článku [evidence času pro auditorskou firmu](https://madsalt.tech/cs/blog/time-tracking-for-audit-firms/). [Kontrola zabezpečení Supabase](https://madsalt.tech/cs/tools/supabase-security-check/) ř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í.
