Vai al contenuto

NULL in IF NOT di PL/pgSQL: controllo saltato su 3683 task

Postgres

Visualizza come MarkdownApri in ClaudeApri in ChatGPTApri in Perplexity

Anche la domanda viene copiata. Se la chat si apre vuota, la incolli.

In breve

Un controllo dei permessi dentro una funzione SECURITY DEFINER diceva if not (has_perm or created_by = me or assignee_id = me) then raise. A giugno 2026, tutte le 3683 attività dell’app non avevano né creatore né assegnatario. false OR NULL OR NULL è NULL, NOT NULL è ancora NULL, e IF salta il suo corpo quando la condizione è NULL. Quindi l’errore non veniva mai sollevato, e qualsiasi utente autenticato poteva clonare le attività. La correzione è coalesce(..., false) attorno a tutta la condizione.

Succedeva nel time tracker della società di revisione, nella funzione dietro “clona attività”.

Com’era il controllo?

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;

Si legge come “rifiuta, a meno che tu possa creare attività, l’abbia creata o ne sia il responsabile”. Le 3683 attività erano state create in automatico, quindi created_by e assignee_id erano NULL su tutte. Per un ruolo con ogni permesso disattivato, la condizione era NOT (false OR NULL OR NULL), cioè NULL, e il raise non scattava mai.

Perché la stessa condizione è sicura nella RLS?

La row level security tratta tutto ciò che non è true come un rifiuto, quindi NULL vuol dire no. Un IF di PL/pgSQL tratta tutto ciò che non è true come “salta il corpo”, e qui il corpo era il rifiuto. La documentazione di PostgreSQL afferma entrambe le metà, per la row security e per IF; insieme trasformano la stessa espressione in un divieto in un posto e in un permesso nell’altro.

Lo stesso NULL, due risposte opposte. has_perm è false, created_by e assignee_id sono NULL. In una policy RLS: false OR NULL → NULL → Riga rifiutata; In IF NOT (...) THEN RAISE: false OR NULL → NOT NULL è NULL → RAISE saltato → Clone consentito.
Logica a tre valori come la descrive la documentazione di PostgreSQL 18, sezioni 5.9 (row security) e 41.6.4 (IF).

Una funzione SECURITY DEFINER inoltre aggira la RLS, quindi il suo controllo è l’unico che vale. Basta un ramo NULL: gli altri possono essere tutti false.

Cosa l’ha risolto?

Avvolgere l’intera condizione così che NULL diventi un rifiuto, e trattare il clone come la creazione di un’attività, non come la modifica dell’originale:

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

È successo di nuovo?

Sì, circa tre mesi dopo, in altre tre funzioni: archiviare un’attività, cambiarne il tipo e verificare se vi si possono registrare ore. Leggere l’SQL non le ha fatte emergere. La prova della RLS ruolo per ruolo sì, perché disattiva ogni permesso e verifica che l’azione venga rifiutata.

Come cercare il problema?

grep 'if not (' trova la sintassi, non il bug. Cosa ha funzionato:

  1. Estrarre il corpo di ogni funzione con pg_get_functiondef in una sola query.
  2. In ogni if not (...) then e if a <> b then, togliere coalesce(...), exists(...) e (select ...). Una catena di or o un confronto su una colonna nullable, senza altro, è un candidato.
  3. Cercare anche if not <variabile> then e risalire a dove la variabile viene impostata. _ok := has_perm(...) or created_by = me; nasconde lo stesso bug in due righe.
  4. Chiamare ogni helper usato come if not helper(...) in tre modi, dentro una transazione annullata: un id sconosciuto, un argomento NULL e un utente reale la cui riga attesa non esiste. Un helper che restituisce NULL in uno qualsiasi dei tre ha bisogno di un coalesce nel punto di chiamata o al suo interno.

In quell’app la ricerca ha controllato 14 helper, tutti già sicuri. Il bug era limitato alla funzione di clone e alle tre trovate dopo. Una lacuna collegata nelle app multi-tenant: cosa la RLS non vede per un utente in due org. L’app era il time tracker di una società di revisione; i suoi altri errori silenziosi sono in timesheet per società di revisione. Il Controllo sicurezza Supabase elenca le funzioni SECURITY DEFINER tra le sue 46 regole, ma non può dire se un IF al loro interno fallisce in modo aperto, quindi questa ricerca resta manuale.

© 2026 Denys Zaichenko. Articolo originale: https://madsalt.tech/it/notes/plpgsql-if-not-null-fails-open/