Gå til indhold

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

Postgres

Vis som MarkdownÅbn i ClaudeÅbn i ChatGPTÅbn i Perplexity

Spørgsmålet kopieres også. Hvis chatten åbner tom, så indsæt det.

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, i funktionen bag “klon opgave”.

Hvordan så tjekket ud?

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 og for IF; 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.
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:

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 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. Appen var et revisionsfirmas tidsregistrering; dens øvrige skjulte fejl står i tidsregistrering for revisionsfirmaer. Supabase sikkerhedstjek 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.

© 2026 Denys Zaichenko. Original artikel: https://madsalt.tech/da/notes/plpgsql-if-not-null-fails-open/