---
title: "NULL im PL/pgSQL-IF NOT: Prüfung bei 3683 Tasks übersprungen"
description: "In einer SECURITY DEFINER-Funktion ist false OR NULL gleich NULL, und IF NOT überspringt das RAISE. Warum RLS sicher ist, die coalesce-Lösung, die Suche."
url: "https://madsalt.tech/de/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "de"
published: 2026-10-18
---

# NULL im PL/pgSQL-IF NOT: Prüfung bei 3683 Tasks übersprungen

Kurz gesagt

Eine Berechtigungsprüfung in einer `SECURITY DEFINER`-Funktion lautete `if not (has_perm or created_by = me or assignee_id = me) then raise`. Im Juni 2026 hatten alle 3683 Aufgaben der App weder Ersteller noch zuständige Person. `false OR NULL OR NULL` ist `NULL`, `NOT NULL` ist weiterhin `NULL`, und `IF` überspringt seinen Rumpf, wenn die Bedingung `NULL` ist. Der Fehler wurde also nie ausgelöst, und jeder angemeldete Nutzer konnte Aufgaben klonen. Die Lösung ist `coalesce(..., false)` um die ganze Bedingung.

Das war im [Zeiterfassungs-Tool für eine Wirtschaftsprüfungsgesellschaft](https://madsalt.tech/de/work/audit-firm-time-tracking/), in der Funktion hinter "Aufgabe klonen".

## Wie sah die Prüfung aus?

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

Sie liest sich als "ablehnen, außer man darf Aufgaben anlegen, hat diese erstellt oder ist dafür zuständig". Die 3683 Aufgaben waren automatisch angelegt worden, daher waren `created_by` und `assignee_id` bei jeder einzelnen `NULL`. Für eine Rolle mit allen ausgeschalteten Rechten lautete die Bedingung `NOT (false OR NULL OR NULL)`, also `NULL`, und das `raise` lief nie.

## Warum ist dieselbe Bedingung in RLS sicher?

Row Level Security behandelt alles, was nicht `true` ist, als Ablehnung, `NULL` heißt also nein. Ein PL/pgSQL-`IF` behandelt alles, was nicht `true` ist, als "Rumpf überspringen", und hier war der Rumpf die Ablehnung. Die PostgreSQL-Dokumentation nennt beide Hälften, für [Row Security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) und für [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); zusammen machen sie aus demselben Ausdruck an einer Stelle ein Verbot und an der anderen eine Erlaubnis.

![Dasselbe NULL, zwei gegensätzliche Antworten. has_perm ist false, created_by und assignee_id sind NULL. In einer RLS-Policy: false OR NULL → NULL → Zeile abgelehnt; In IF NOT (...) THEN RAISE: false OR NULL → NOT NULL ist NULL → RAISE übersprungen → Klonen erlaubt.](https://madsalt.tech/charts/de/plpgsql-if-not-null-fails-open.svg)

*Dreiwertige Logik, wie die PostgreSQL-18-Dokumentation sie beschreibt, Abschnitte 5.9 (Row Security) und 41.6.4 (IF).*

Eine `SECURITY DEFINER`-Funktion läuft außerdem an RLS vorbei, ihre eigene Prüfung ist also die einzige, die gilt. Ein einziger `NULL`-Zweig genügt: Die anderen Zweige können alle `false` sein.

## Was hat es behoben?

Schließen Sie die ganze Bedingung ein, sodass `NULL` zur Ablehnung wird, und behandeln Sie das Klonen als Anlegen einer Aufgabe, nicht als Bearbeiten der Quelle:

```sql
if not coalesce((select has_perm('tasks', 'create')), false) then
  raise exception 'forbidden' using errcode = '42501';
end if;
```

## Ist es wieder passiert?

Ja, etwa drei Monate später, in drei weiteren Funktionen: beim Archivieren einer Aufgabe, beim Ändern ihres Typs und bei der Prüfung, ob darauf Zeit gebucht werden darf. Das Lesen des SQL fing sie nicht ab. Der [Nachweis der RLS Rolle für Rolle](https://madsalt.tech/de/blog/how-to-test-supabase-rls/) tat es, denn er schaltet jedes Recht aus und prüft, dass die Aktion abgelehnt wird.

## Wie suchen Sie danach?

`grep 'if not ('` findet die Syntax, nicht den Fehler. Was funktioniert hat:

1. Alle Funktionsrümpfe mit `pg_get_functiondef` in einer Abfrage ausgeben.
2. In jedem `if not (...) then` und `if a <> b then` `coalesce(...)`, `exists(...)` und `(select ...)` entfernen. Eine nackte `or`-Kette oder ein Vergleich über eine Spalte, die NULL sein kann, ist ein Kandidat.
3. Auch `if not <variable> then` suchen und nachverfolgen, wo die Variable gesetzt wird. `_ok := has_perm(...) or created_by = me;` versteckt denselben Fehler in zwei Zeilen.
4. Jeden Helfer, der als `if not helper(...)` verwendet wird, in einer zurückgerollten Transaktion auf drei Arten aufrufen: mit einer unbekannten ID, mit einem `NULL`-Argument und mit einem echten Nutzer, dessen erwartete Zeile fehlt. Ein Helfer, der dabei irgendwann `NULL` liefert, braucht ein `coalesce` an der Aufrufstelle oder in sich.

In dieser App prüfte die Suche 14 Helfer, alle bereits sicher. Der Fehler beschränkte sich auf die Klon-Funktion und die drei später gefundenen. Eine verwandte Lücke bei mandantenfähigen Apps: [was RLS bei einem Nutzer in zwei Organisationen übersieht](https://madsalt.tech/de/notes/supabase-rls-user-in-two-organizations/). Die App war der Zeiterfassungs-Tracker einer Wirtschaftsprüfungskanzlei; ihre übrigen stillen Fehler stehen in [Zeiterfassung für Wirtschaftsprüfer](https://madsalt.tech/de/blog/time-tracking-for-audit-firms/). Der [Supabase Sicherheitscheck](https://madsalt.tech/de/tools/supabase-security-check/) listet SECURITY DEFINER-Funktionen unter seinen 46 Regeln, kann aber nicht erkennen, ob ein IF darin offen fehlschlägt, daher bleibt dieser Durchgang manuell.
