Saltar para o conteúdo

Supabase RLS multi-tenant: utilizador em duas orgs

Supabase

Ver como MarkdownAbrir em ClaudeAbrir em ChatGPTAbrir em Perplexity

A pergunta também é copiada. Se o chat abrir vazio, cole-a.

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.

A org do pedido não está na política. Um utilizador, membro da org A e da org B, a trabalhar na org A. Só a política de RLS: Pedido na org A → Membro da org da linha? → Sim, A ou B → Linha da org B; RLS mais a org deste pedido: Pedido na org A → Membro da org da linha? → Linha na org A? → 0 linhas da org B.
A minha sonda com um utilizador de teste em duas orgs, setembro de 2026: só com RLS, leitura e escrita no documento da org B; com a org do pedido, 0 linhas.

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.

© 2026 Denys Zaichenko. Artigo original: https://madsalt.tech/pt/notes/supabase-rls-user-in-two-organizations/