---
title: "NULL i PL/pgSQL IF NOT hoppet over sjekken på 3683 oppgaver"
description: "I en SECURITY DEFINER-funksjon er false OR NULL lik NULL, og IF NOT hopper over RAISE. Hvorfor RLS er trygt, coalesce-løsningen og hvordan du søker."
url: "https://madsalt.tech/no/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "no"
published: 2026-10-18
---

# NULL i PL/pgSQL IF NOT hoppet over sjekken på 3683 oppgaver

TL;DR

En tilgangssjekk i en `SECURITY DEFINER`-funksjon lød `if not (has_perm or created_by = me or assignee_id = me) then raise`. I juni 2026 manglet alle 3683 oppgaver i appen både oppretter og ansvarlig. `false OR NULL OR NULL` er `NULL`, `NOT NULL` er fortsatt `NULL`, og `IF` hopper over kroppen sin når betingelsen er `NULL`. Derfor ble feilen aldri kastet, og enhver innlogget bruker kunne klone oppgaver. Løsningen er `coalesce(..., false)` rundt hele betingelsen.

Dette skjedde i [timeregistreringssystemet for revisjonsselskapet](https://madsalt.tech/no/work/audit-firm-time-tracking/), i funksjonen bak "klon oppgave".

## Hvordan så sjekken ut?

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

Den leses som "avvis med mindre du kan opprette oppgaver, har laget denne eller eier den". De 3683 oppgavene var seedet automatisk, så `created_by` og `assignee_id` var `NULL` på hver eneste en. For en rolle med alle tilganger slått av ble betingelsen `NOT (false OR NULL OR NULL)`, altså `NULL`, og `raise` ble aldri kjørt.

## Hvorfor er samme betingelse trygg i RLS?

Row level security behandler alt som ikke er `true` som et avslag, så `NULL` betyr nei. Et PL/pgSQL-`IF` behandler alt som ikke er `true` som "hopp over kroppen", og her var kroppen selve avslaget. PostgreSQL-dokumentasjonen sier begge halvdelene, 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 gjør de samme uttrykk til et avslag ett sted og en tillatelse et annet.

![Samme NULL, to motsatte svar. has_perm er false, created_by og assignee_id er NULL. I en RLS-policy: false OR NULL → NULL → Raden avvises; I IF NOT (...) THEN RAISE: false OR NULL → NOT NULL er NULL → RAISE hoppes over → Kloning tillatt.](https://madsalt.tech/charts/no/plpgsql-if-not-null-fails-open.svg)

*Trevalenslogikk slik PostgreSQL 18-dokumentasjonen beskriver den, seksjon 5.9 (row security) og 41.6.4 (IF).*

En `SECURITY DEFINER`-funksjon kjører dessuten utenom RLS, så dens egen sjekk er den eneste som gjelder. Én `NULL`-gren er nok: de andre grenene kan alle være `false`.

## Hva løste det?

Pakk inn hele betingelsen slik at `NULL` blir et avslag, og behandle kloning som å opprette en oppgave, ikke som å redigere kilden:

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

## Skjedde det igjen?

Ja, omtrent tre måneder senere, i tre funksjoner til: å arkivere en oppgave, å endre typen og å sjekke om tid kan registreres på den. Å lese SQL-koden fanget dem ikke. [Beviset rolle for rolle for RLS](https://madsalt.tech/no/blog/how-to-test-supabase-rls/) gjorde det, fordi det slår av hver tilgang og sjekker at handlingen avvises.

## Hvordan søker du gjennom koden?

`grep 'if not ('` finner syntaksen, ikke feilen. Dette fungerte:

1. Dump hver funksjonskropp med `pg_get_functiondef` i én spørring.
2. Fjern i hver `if not (...) then` og `if a <> b then` `coalesce(...)`, `exists(...)` og `(select ...)`. En naken `or`-kjede eller sammenligning over en kolonne som kan være null, er en kandidat.
3. Match også `if not <variable> then` og følg hvor variabelen settes. `_ok := has_perm(...) or created_by = me;` skjuler samme feil på to linjer.
4. Kall hver hjelpefunksjon som brukes som `if not helper(...)` på tre måter i en transaksjon som rulles tilbake: en ukjent id, et `NULL`-argument og en ekte bruker der den forventede raden mangler. En hjelpefunksjon som returnerer `NULL` i noen av tilfellene, trenger en `coalesce` ved kallet eller inni.

I den appen sjekket gjennomgangen 14 hjelpefunksjoner, og alle var allerede trygge. Feilen var begrenset til klonfunksjonen og de tre som ble funnet senere. Et beslektet multi-tenant-hull: [hva RLS overser for en bruker i to organisasjoner](https://madsalt.tech/no/notes/supabase-rls-user-in-two-organizations/). Appen var et revisjonsselskaps timeregistrering; de andre stille feilene står i [timeregistrering for revisjonsselskap](https://madsalt.tech/no/blog/time-tracking-for-audit-firms/). [Supabase sikkerhetssjekk](https://madsalt.tech/no/tools/supabase-security-check/) lister SECURITY DEFINER-funksjoner blant sine 46 regler, men kan ikke se om en IF inni en av dem feiler åpent, så denne gjennomgangen er fortsatt manuell.
