---
title: "NULL i PL/pgSQL IF NOT släppte igenom 3683 uppgifter"
description: "I en SECURITY DEFINER-funktion är false OR NULL lika med NULL och IF NOT hoppar över RAISE. Varför RLS är säkert, coalesce-åtgärden och hur du söker."
url: "https://madsalt.tech/sv/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "sv"
published: 2026-10-18
---

# NULL i PL/pgSQL IF NOT släppte igenom 3683 uppgifter

Kort sagt

En behörighetskontroll i en `SECURITY DEFINER`-funktion löd `if not (has_perm or created_by = me or assignee_id = me) then raise`. I juni 2026 saknade alla 3683 uppgifter i appen både skapare och ansvarig. `false OR NULL OR NULL` är `NULL`, `NOT NULL` är fortfarande `NULL`, och `IF` hoppar över sin kropp när villkoret är `NULL`. Därför kastades felet aldrig, och vilken inloggad användare som helst kunde klona uppgifter. Åtgärden är `coalesce(..., false)` runt hela villkoret.

Det här hände i [tidrapporteringssystemet för revisionsbyrån](https://madsalt.tech/sv/work/audit-firm-time-tracking/), i funktionen bakom "klona uppgift".

## Hur såg kontrollen 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 läses som "neka om du inte får skapa uppgifter, inte har gjort den här eller inte äger den". De 3683 uppgifterna hade seedats automatiskt, så `created_by` och `assignee_id` var `NULL` på varenda en. För en roll med alla behörigheter avstängda blev villkoret `NOT (false OR NULL OR NULL)`, alltså `NULL`, och `raise` kördes aldrig.

## Varför är samma villkor säkert i RLS?

Row level security behandlar allt som inte är `true` som ett nekande, så `NULL` betyder nej. Ett PL/pgSQL-`IF` behandlar allt som inte är `true` som "hoppa över kroppen", och här var kroppen själva nekandet. PostgreSQL-dokumentationen anger båda halvorna, för [row security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) och för [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); tillsammans gör de samma uttryck till ett nekande på ett ställe och ett tillåtande på det andra.

![Samma NULL, två motsatta svar. has_perm är false, created_by och assignee_id är NULL. I en RLS-policy: false OR NULL → NULL → Raden nekas; I IF NOT (...) THEN RAISE: false OR NULL → NOT NULL är NULL → RAISE hoppas över → Kloning tillåten.](https://madsalt.tech/charts/sv/plpgsql-if-not-null-fails-open.svg)

*Trevärdeslogik så som PostgreSQL 18-dokumentationen beskriver den, avsnitt 5.9 (row security) och 41.6.4 (IF).*

En `SECURITY DEFINER`-funktion körs dessutom förbi RLS, så dess egen kontroll är den enda som gäller. En enda `NULL`-gren räcker: de andra grenarna kan alla vara `false`.

## Vad åtgärdade det?

Linda in hela villkoret så att `NULL` blir ett nekande, och behandla kloning som att skapa en uppgift, inte som att redigera källan:

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

## Hände det igen?

Ja, ungefär tre månader senare, i tre funktioner till: att arkivera en uppgift, att ändra dess typ och att kontrollera om tid får registreras på den. Att läsa SQL-koden fångade dem inte. [Beviset roll för roll för RLS](https://madsalt.tech/sv/blog/how-to-test-supabase-rls/) gjorde det, eftersom det stänger av varje behörighet och kontrollerar att åtgärden nekas.

## Hur söker du igenom koden?

`grep 'if not ('` hittar syntaxen, inte buggen. Det här fungerade:

1. Dumpa varje funktionskropp med `pg_get_functiondef` i en enda fråga.
2. Ta i varje `if not (...) then` och `if a <> b then` bort `coalesce(...)`, `exists(...)` och `(select ...)`. En naken `or`-kedja eller jämförelse över en kolumn som kan vara null är en kandidat.
3. Matcha också `if not <variable> then` och följ var variabeln sätts. `_ok := has_perm(...) or created_by = me;` döljer samma bugg på två rader.
4. Anropa varje hjälpfunktion som används som `if not helper(...)` på tre sätt i en transaktion som rullas tillbaka: ett okänt id, ett `NULL`-argument och en verklig användare vars förväntade rad saknas. En hjälpfunktion som returnerar `NULL` i något av fallen behöver en `coalesce` vid anropet eller inuti.

I den appen kontrollerade genomgången 14 hjälpfunktioner, som alla redan var säkra. Buggen var begränsad till klonfunktionen och de tre som hittades senare. Ett närliggande multi-tenant-glapp: [vad RLS missar för en användare i två organisationer](https://madsalt.tech/sv/notes/supabase-rls-user-in-two-organizations/). Appen var en revisionsbyrås tidrapportering; dess övriga tysta fel finns i [tidrapportering för revisionsbyråer](https://madsalt.tech/sv/blog/time-tracking-for-audit-firms/). [Supabase säkerhetskontroll](https://madsalt.tech/sv/tools/supabase-security-check/) listar SECURITY DEFINER-funktioner bland sina 46 regler, men kan inte avgöra om ett IF inuti en av dem fallerar öppet, så den här genomgången är fortfarande manuell.
