---
title: "NULL num IF NOT de PL/pgSQL saltou a regra em 3683 tarefas"
description: "Numa função SECURITY DEFINER, false OR NULL é NULL e o IF NOT salta o RAISE. Porque o RLS é seguro com a mesma condição, a correção com coalesce e a pesquisa."
url: "https://madsalt.tech/pt/notes/plpgsql-if-not-null-fails-open/"
author: "Denys Zaichenko"
language: "pt"
published: 2026-10-18
---

# NULL num IF NOT de PL/pgSQL saltou a regra em 3683 tarefas

Em resumo

Uma verificação de permissões dentro de uma função `SECURITY DEFINER` lia `if not (has_perm or created_by = me or assignee_id = me) then raise`. Em junho de 2026, as 3683 tarefas da aplicação não tinham criador nem responsável. `false OR NULL OR NULL` é `NULL`, `NOT NULL` continua `NULL`, e o `IF` salta o corpo quando a condição é `NULL`. Por isso o erro nunca era lançado, e qualquer utilizador com sessão iniciada podia clonar tarefas. A correção é `coalesce(..., false)` à volta de toda a condição.

Isto foi no [registo de horas da empresa de auditoria](https://madsalt.tech/pt/work/audit-firm-time-tracking/), na função por trás de "clonar tarefa".

## Como era a verificação?

```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;
```

Lê-se como "recusar, a menos que possa criar tarefas, a tenha criado ou seja o responsável". As 3683 tarefas tinham sido criadas automaticamente, por isso `created_by` e `assignee_id` eram `NULL` em todas. Para uma função com todas as permissões desligadas, a condição era `NOT (false OR NULL OR NULL)`, que é `NULL`, e o `raise` nunca executava.

## Porque é que a mesma condição é segura no RLS?

O row level security trata tudo o que não é `true` como recusa, por isso `NULL` significa não. Um `IF` de PL/pgSQL trata tudo o que não é `true` como "saltar o corpo", e aqui o corpo era a recusa. A documentação do PostgreSQL descreve as duas metades, para [row security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) e para [IF](https://www.postgresql.org/docs/current/plpgsql-control-structures.html); juntas, fazem da mesma expressão uma recusa num sítio e uma autorização no outro.

![O mesmo NULL, duas respostas opostas. has_perm é false, created_by e assignee_id são NULL. Numa política de RLS: false OR NULL → NULL → Linha recusada; Em IF NOT (...) THEN RAISE: false OR NULL → NOT NULL é NULL → RAISE saltado → Clone permitido.](https://madsalt.tech/charts/pt/plpgsql-if-not-null-fails-open.svg)

*Lógica de três valores como a descreve a documentação do PostgreSQL 18, secções 5.9 (row security) e 41.6.4 (IF).*

Uma função `SECURITY DEFINER` também passa por cima do RLS, por isso a sua própria verificação é a única que vale. Basta um ramo `NULL`: os outros podem ser todos `false`.

## O que resolveu?

Envolver toda a condição para que `NULL` passe a ser recusa, e tratar a clonagem como a criação de uma tarefa, não como a edição da original:

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

## Voltou a acontecer?

Sim, cerca de três meses depois, em mais três funções: arquivar uma tarefa, mudar-lhe o tipo e verificar se se pode registar tempo nela. Ler o SQL não as apanhou. A [prova de RLS função a função](https://madsalt.tech/pt/blog/how-to-test-supabase-rls/) apanhou, porque desliga cada permissão e verifica que a ação é recusada.

## Como procurar o problema?

`grep 'if not ('` encontra a sintaxe, não o bug. O que funcionou:

1. Extrair o corpo de todas as funções com `pg_get_functiondef`, numa só consulta.
2. Em cada `if not (...) then` e `if a <> b then`, retirar `coalesce(...)`, `exists(...)` e `(select ...)`. Uma cadeia de `or` ou uma comparação sobre uma coluna anulável, sem mais nada, é candidata.
3. Procurar também `if not <variável> then` e seguir onde a variável é definida. `_ok := has_perm(...) or created_by = me;` esconde o mesmo bug em duas linhas.
4. Chamar cada helper usado como `if not helper(...)` de três formas, dentro de uma transação revertida: um id desconhecido, um argumento `NULL` e um utilizador real cuja linha esperada não existe. Um helper que devolva `NULL` em qualquer delas precisa de `coalesce` no ponto de chamada ou dentro dele.

Nessa aplicação, a pesquisa verificou 14 helpers, todos já seguros. O bug ficou restrito à função de clonagem e às três encontradas mais tarde. Uma falha relacionada em aplicações multi-tenant: [o que o RLS não apanha num utilizador em duas orgs](https://madsalt.tech/pt/notes/supabase-rls-user-in-two-organizations/). A aplicação era o registo de horas de uma sociedade de auditoria; os seus outros erros silenciosos estão em [registo de horas em sociedades de auditoria](https://madsalt.tech/pt/blog/time-tracking-for-audit-firms/). A [Auditoria de segurança Supabase](https://madsalt.tech/pt/tools/supabase-security-check/) inclui as funções SECURITY DEFINER entre as suas 46 regras, mas não consegue saber se um IF dentro de uma delas falha em aberto, por isso esta pesquisa continua manual.
