---
title: "En NULL i PL/pgSQL IF NOT sprang tjek over for 3683 opgaver"
description: "I en SECURITY DEFINER-funktion er false OR NULL lig NULL, og IF NOT springer RAISE over. Hvorfor tjekket er sikkert i RLS, coalesce-rettelsen og søgningen."
url: "https://madsalt.tech/da/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "da"
published: 2026-10-18
---

# En NULL i PL/pgSQL IF NOT sprang tjek over for 3683 opgaver

TL;DR

Et rettighedstjek i en `SECURITY DEFINER`-funktion lød `if not (has_perm or created_by = me or assignee_id = me) then raise`. I juni 2026 havde alle 3683 opgaver i appen hverken en opretter eller en ansvarlig. `false OR NULL OR NULL` er `NULL`, `NOT NULL` er stadig `NULL`, og `IF` springer sin krop over, når betingelsen er `NULL`. Så fejlen blev aldrig kastet, og enhver indlogget bruger kunne klone opgaver. Rettelsen er `coalesce(..., false)` omkring hele betingelsen.

Det skete i [tidsregistreringen til et revisionsfirma](https://madsalt.tech/da/work/audit-firm-time-tracking/), i funktionen bag "klon opgave".

## Hvordan så tjekket ud?

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

Det læses som "afvis, medmindre du må oprette opgaver, har lavet denne eller ejer den". De 3683 opgaver var oprettet automatisk, så `created_by` og `assignee_id` var `NULL` på hver eneste. For en rolle med alle rettigheder slået fra var betingelsen `NOT (false OR NULL OR NULL)`, som er `NULL`, og `raise` kørte aldrig.

## Hvorfor er samme betingelse sikker i RLS?

Row level security behandler alt, der ikke er `true`, som et afslag, så `NULL` betyder nej. Et PL/pgSQL-`IF` behandler alt, der ikke er `true`, som "spring kroppen over", og her var kroppen selve afslaget. PostgreSQL-dokumentationen angiver begge halvdele, for [row security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) og for [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); sammen gør de det samme udtryk til et afslag det ene sted og en tilladelse det andet.

![Den samme NULL, to modsatte svar. has_perm er false, created_by og assignee_id er NULL. I en RLS-policy: false OR NULL → NULL → Række afvist; I IF NOT (...) THEN RAISE: false OR NULL → NOT NULL er NULL → RAISE sprunget over → Kloning tilladt.](https://madsalt.tech/charts/da/plpgsql-if-not-null-fails-open.svg)

*Trevædiglogik som beskrevet i PostgreSQL 18-dokumentationen, afsnit 5.9 (row security) og 41.6.4 (IF).*

En `SECURITY DEFINER`-funktion kører desuden uden om RLS, så dens eget tjek er det eneste, der gælder. Én `NULL`-gren er nok: de andre kan alle være `false`.

## Hvad rettede det?

Pak hele betingelsen ind, så `NULL` bliver et afslag, og behandl kloning som at oprette en opgave, ikke som at redigere kilden:

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

## Skete det igen?

Ja, cirka tre måneder senere, i tre funktioner mere: arkivering af en opgave, ændring af dens type og tjek af, om der må registreres tid på den. At læse SQL'en fangede dem ikke. [Beviset for RLS rolle for rolle](https://madsalt.tech/da/blog/how-to-test-supabase-rls/) gjorde, fordi det slår hver rettighed fra og tjekker, at handlingen afvises.

## Hvordan finder du dem alle?

`grep 'if not ('` finder syntaksen, ikke fejlen. Det, der virkede:

1. Hent alle funktionsdefinitioner med `pg_get_functiondef` i én forespørgsel.
2. I hvert `if not (...) then` og `if a <> b then`: fjern `coalesce(...)`, `exists(...)` og `(select ...)`. En bar `or`-kæde eller sammenligning over en kolonne, der kan være null, er en kandidat.
3. Match også `if not <variable> then`, og find ud af, hvor variablen sættes. `_ok := has_perm(...) or created_by = me;` skjuler den samme fejl på to linjer.
4. Kald hver helper, der bruges som `if not helper(...)`, på tre måder i en transaktion, der rulles tilbage: et ukendt id, et `NULL`-argument og en rigtig bruger, hvis forventede række mangler. En helper, der returnerer `NULL` i en af dem, har brug for en `coalesce` ved kaldet eller indeni.

I den app tjekkede gennemgangen 14 helpers, og alle var allerede sikre. Fejlen var begrænset til klonfunktionen og de tre, der blev fundet senere. Et beslægtet hul i multi-tenant-apps: [hvad RLS overser for en bruger i to orgs](https://madsalt.tech/da/notes/supabase-rls-user-in-two-organizations/). Appen var et revisionsfirmas tidsregistrering; dens øvrige skjulte fejl står i [tidsregistrering for revisionsfirmaer](https://madsalt.tech/da/blog/time-tracking-for-audit-firms/). [Supabase sikkerhedstjek](https://madsalt.tech/da/tools/supabase-security-check/) lister SECURITY DEFINER-funktioner blandt sine 46 regler, men kan ikke se, om en IF inde i en af dem fejler åbent, så denne gennemgang er stadig manuel.
