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.
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:
- Estrarre il corpo di ogni funzione con
pg_get_functiondefin una sola query. - In ogni
if not (...) theneif a <> b then, toglierecoalesce(...),exists(...)e(select ...). Una catena dioro un confronto su una colonna nullable, senza altro, è un candidato. - Cercare anche
if not <variabile> thene risalire a dove la variabile viene impostata._ok := has_perm(...) or created_by = me;nasconde lo stesso bug in due righe. - Chiamare ogni helper usato come
if not helper(...)in tre modi, dentro una transazione annullata: un id sconosciuto, un argomentoNULLe un utente reale la cui riga attesa non esiste. Un helper che restituisceNULLin uno qualsiasi dei tre ha bisogno di uncoalescenel 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.