---
title: "NULL in IF NOT di PL/pgSQL: controllo saltato su 3683 task"
description: "In una funzione SECURITY DEFINER, false OR NULL è NULL e IF NOT salta il RAISE. Perché nella RLS è sicuro, la correzione con coalesce e come cercare il bug."
url: "https://madsalt.tech/it/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "it"
published: 2026-10-18
---

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

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](https://madsalt.tech/it/work/audit-firm-time-tracking/), nella funzione dietro "clona attività".

## Com'era il controllo?

```sql
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](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) e per [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); 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.](https://madsalt.tech/charts/it/plpgsql-if-not-null-fails-open.svg)

*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:

```sql
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](https://madsalt.tech/it/blog/how-to-test-supabase-rls/) 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](https://madsalt.tech/it/notes/supabase-rls-user-in-two-organizations/). L'app era il time tracker di una società di revisione; i suoi altri errori silenziosi sono in [timesheet per società di revisione](https://madsalt.tech/it/blog/time-tracking-for-audit-firms/). Il [Controllo sicurezza Supabase](https://madsalt.tech/it/tools/supabase-security-check/) 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.
