Hopp til innhold

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

Postgres

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

Spørsmålet kopieres også. Hvis chatten åpnes tom, lim det inn.

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, i funksjonen bak “klon oppgave”.

Hvordan så sjekken ut?

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

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 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. Appen var et revisjonsselskaps timeregistrering; de andre stille feilene står i timeregistrering for revisjonsselskap. Supabase sikkerhetssjekk 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.

© 2026 Denys Zaichenko. Opprinnelig artikkel: https://madsalt.tech/no/notes/plpgsql-if-not-null-fails-open/