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.
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:
- Hent alle funktionsdefinitioner med
pg_get_functiondefi én forespørgsel. - I hvert
if not (...) thenogif a <> b then: fjerncoalesce(...),exists(...)og(select ...). En baror-kæde eller sammenligning over en kolonne, der kan være null, er en kandidat. - 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. - Kald hver helper, der bruges som
if not helper(...), på tre måder i en transaktion, der rulles tilbage: et ukendt id, etNULL-argument og en rigtig bruger, hvis forventede række mangler. En helper, der returnererNULLi en af dem, har brug for encoalesceved 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.