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, na função por trás de “clonar tarefa”.
Como era a verificação?
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 e para IF; juntas, fazem da mesma expressão uma recusa num sítio e uma autorização no outro.
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:
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 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:
- Extrair o corpo de todas as funções com
pg_get_functiondef, numa só consulta. - Em cada
if not (...) theneif a <> b then, retirarcoalesce(...),exists(...)e(select ...). Uma cadeia deorou uma comparação sobre uma coluna anulável, sem mais nada, é candidata. - Procurar também
if not <variável> thene seguir onde a variável é definida._ok := has_perm(...) or created_by = me;esconde o mesmo bug em duas linhas. - Chamar cada helper usado como
if not helper(...)de três formas, dentro de uma transação revertida: um id desconhecido, um argumentoNULLe um utilizador real cuja linha esperada não existe. Um helper que devolvaNULLem qualquer delas precisa decoalesceno 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. 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. A Auditoria de segurança Supabase 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.