Em resumo
Uma política como auth_in_org(organization_id) pergunta “o utilizador é membro da org a que esta linha pertence?”. Para um utilizador que pertence à org A e à org B, a resposta é sim nas duas. Na minha sonda, só com RLS esse utilizador tinha acesso de leitura e escrita a um documento da outra org. Ao acrescentar à consulta a org do pedido atual, o resultado foi 0 linhas.
Encontrei isto ao reforçar a plataforma de documentos de construção, uma aplicação Supabase multi-tenant. Todas as tabelas de tenant tinham row level security ativo e forçado, por isso parecia concluída.
O que parecia seguro?
Todas as políticas tinham a mesma forma: o organization_id da linha tem de ser uma org de que o utilizador é membro. Por isso, algumas server actions procuravam um documento só pelo id. O raciocínio era “o RLS filtra de qualquer forma”.
const { data } = await supabase
.from('documents')
.select('*')
.eq('document_id', id)
.single()
Para um utilizador com uma só pertença isto funciona, e um teste com esse utilizador passa.
Porque é que o RLS deixa passar a outra org?
A política é um teste de pertença. O Postgres avalia-a linha a linha, como descreve a documentação de row security, e não sabe em nome de que organização o pedido está a agir, porque isso vive na aplicação: no URL, num cookie ou no seletor de orgs. Para um utilizador que é membro das duas orgs, todas as linhas das duas passam.
Ou seja, o filtro por organization_id na aplicação não é uma segunda camada por cima do RLS. Para este utilizador, é o único controlo.
Já que está nesses helpers, verifique se olham para organizations.is_active. Os helpers de pertença muitas vezes não olham, e então uma org desativada mantém acesso de leitura e escrita na camada de RLS. Quando a única verificação de is_active vive no resolvedor de tenant da aplicação, qualquer consulta que ignore o resolvedor ignora também a desativação.
O que resolveu?
A org do pedido passou a ser um argumento obrigatório de todas as funções de serviço que leem ou escrevem dados de tenant. Um ponto de chamada não se pode esquecer dela, porque a função não compila sem ela.
export async function getDocument(orgId: string, id: string) {
return supabase
.from('documents')
.select('*')
.eq('organization_id', orgId)
.eq('document_id', id)
.single()
}
A revisão deu mais duas regras:
- Nunca tome a org da linha que vai escrever.
update ... where id = $1 and organization_id = <o organization_id da própria linha>é sempre verdadeiro, por isso não protege nada. - Uma chave estrangeira composta em
(id, organization_id)faz a base de dados recusar uma ligação entre linhas de orgs diferentes. Verifique todas as tabelas filhas: é fácil acrescentá-la a três e esquecer a quarta.
Como verificar a sua aplicação?
Crie um utilizador com uma pertença ativa em duas orgs. Depois, no editor SQL de uma cópia local ou de staging, aja como esse utilizador e peça uma linha da outra org:
begin;
set local role authenticated;
set local request.jwt.claims = '{"sub": "<user id>", "role": "authenticated"}';
-- RLS alone: returns the org B document
select document_id from documents where document_id = '<org B document id>';
-- with the org of the request: 0 rows
select document_id from documents
where document_id = '<org B document id>'
and organization_id = '<org A id>';
rollback;
Repita com um update, e outra vez depois de pôr a segunda org como inativa. Se a primeira consulta devolver uma linha, veja todos os sítios que consultam dados de tenant só pelo id.
Quão grave é?
Dimensione-o com honestidade. O atacante precisa de uma pertença real nas duas orgs, por isso isto é sobre integridade e trilhos de auditoria: alguém com acesso a dois clientes edita o errado, e o registo diz que era permitido. Não é uma forma de um estranho ler todos os tenants.
A lição geral é a de como testo o Supabase RLS: teste o que um utilizador real consegue fazer, com dados reais, e não só o que cada política diz. Três notas relacionadas: uma função tirada dos metadados do registo, um NULL que saltou uma verificação de permissão e que schemas a Data API serve. O teste das duas empresas é a primeira verificação da minha lista de segurança para apps Lovable. Para as políticas, grants e definições à volta, a Auditoria de segurança Supabase gratuita executa 46 regras do OWASP Top 10:2025; se uma política deixa passar um utilizador de duas orgs é uma questão de forma dos dados a que só a consulta acima responde.