Tox

Início/Artigos

Isolamento de dados em SaaS multi-tenant: onde o vazamento entre clientes acontece

Em um SaaS, o dado de um cliente aparecer para outro costuma vir de uma consulta sem filtro, de uma chave que ignora as regras do banco ou de um relatório gerado fora do fluxo normal. Veja os pontos de falha mais comuns, como testar e o que a LGPD exige do operador.

Publicado em 23 de setembro de 2026.

Três formas de separar os clientes

Todo SaaS precisa decidir como separar os dados de cada cliente (tenant). Há três modelos principais, e muitos produtos misturam mais de um.

No banco por cliente, cada empresa tem seu próprio banco de dados. O isolamento é forte porque uma consulta simplesmente não alcança outro banco, mas o custo operacional cresce com o número de clientes: migrações precisam rodar em todos, e backup e monitoramento se multiplicam. No schema por cliente, todos ficam no mesmo servidor, cada um em seu schema. A separação depende de a conexão apontar para o schema certo, e um erro na troca de contexto expõe o cliente anterior.

O modelo mais comum é a tabela compartilhada com uma coluna tenant_id em cada linha. É o mais simples de operar e o mais fácil de errar: basta uma consulta sem WHERE tenant_id = ... para que dados de todos os clientes voltem juntos. O restante deste texto trata principalmente desse modelo, embora quase todos os pontos valham para os outros dois.

IDOR e BOLA: o identificador na requisição

A falha mais frequente é a API que recebe um identificador e busca o registro sem conferir se ele pertence ao cliente de quem pediu. O usuário da empresa A abre GET /api/faturas/8812, troca para 8813 e recebe a fatura da empresa B. A OWASP chama isso de IDOR no Top 10 para aplicações web, dentro de A01:2025 Broken Access Control, e de BOLA (Broken Object Level Authorization) no API Security Top 10, onde ocupa o primeiro lugar da edição 2023.

Trocar identificadores sequenciais por UUIDs dificulta a adivinhação, e a OWASP recomenda identificadores aleatórios. Mesmo assim, UUIDs vazam em URLs compartilhadas, logs e exportações. A correção está em cada função que usa um identificador vindo do cliente verificar, no servidor, se o usuário logado tem direito sobre aquele registro, e em testes automatizados que falhem quando essa verificação some.

O tenant_id usado no filtro precisa vir da sessão autenticada. Se a API aceita tenant_id no corpo da requisição ou em um cabeçalho, basta o atacante mudar o valor.

RLS no PostgreSQL: o filtro dentro do banco

Row Level Security permite que o próprio PostgreSQL filtre as linhas de acordo com políticas, mesmo que a aplicação esqueça o WHERE. Depois de ALTER TABLE faturas ENABLE ROW LEVEL SECURITY, uma política como USING (tenant_id = current_setting('app.tenant_id')::uuid) limita o que cada conexão enxerga. A cláusula USING controla as linhas visíveis e WITH CHECK controla as linhas que podem ser gravadas; quando só USING é informado, ela vale para as duas coisas. Se o RLS estiver ativo e não houver política, o padrão é negar tudo.

Há exceções que a documentação oficial deixa claras. Superusuários e roles com o atributo BYPASSRLS sempre ignoram o RLS. O dono da tabela normalmente também ignora, a menos que a tabela seja configurada com ALTER TABLE ... FORCE ROW LEVEL SECURITY. Isso pega muitos times de surpresa: se a aplicação se conecta com o mesmo usuário que criou as tabelas nas migrações, as políticas simplesmente não se aplicam a ela.

Outros dois detalhes: políticas permissivas se combinam com OR e restritivas com AND, então uma política permissiva genérica criada para um relatório pode abrir a tabela inteira. E verificações de integridade referencial, como chaves únicas e estrangeiras, sempre ignoram o RLS, o que pode permitir inferir a existência de um registro de outro cliente pela mensagem de erro.

Chaves e contas que passam por cima das regras

Mesmo com RLS bem escrito, quase todo sistema tem uma credencial que ignora as políticas: a conta usada por migrações, por rotinas de administração ou por integrações. No Supabase, por exemplo, a documentação informa que a chave secreta acessa o banco pela role service_role, que tem o atributo bypassrls, e orienta a nunca usá-la no navegador.

O problema aparece quando essa credencial vai parar em uma rota comum da API por conveniência. Uma rota que busca um registro por id usando a chave de serviço devolve o registro de qualquer cliente, e o RLS não tem como ajudar. Toda rota que usa credencial privilegiada precisa aplicar o filtro de cliente no código e conferir a permissão do usuário antes da consulta.

Vale fazer um inventário simples: listar onde cada credencial privilegiada é usada e revisar cada uma dessas rotas com a pergunta de onde vem o tenant_id.

Arquivos, relatórios e exportações

Arquivos costumam ficar fora do banco, em buckets de armazenamento, e o isolamento feito nas tabelas não os alcança. Os erros típicos são bucket público com nomes previsíveis como /notas/2026/000123.pdf, links assinados com validade de meses e rotas de download que conferem apenas se o usuário está logado. O caminho do arquivo deveria incluir o identificador do cliente, e a política do bucket ou a rota de download deveria conferir esse prefixo contra a sessão.

Relatórios e exportações são outro ponto frágil porque muitas vezes são escritos depois, por outra pessoa, com consultas próprias e credenciais de serviço para ganhar desempenho. Um CSV de clientes que junta tabelas sem repetir o filtro de tenant em cada JOIN entrega dados de todos. O mesmo vale para dashboards que agregam números: um total calculado sem filtro revela o volume de outros clientes.

Cache, jobs em background e busca

Cache é uma fonte silenciosa de vazamento. Se a chave do cache é relatorio:mensal em vez de relatorio:mensal:tenant_42, o primeiro cliente a gerar o relatório define o que todos os outros verão. O mesmo acontece com respostas de API guardadas por CDN sem variar pela sessão.

Jobs em background rodam fora da requisição e, portanto, sem a sessão do usuário. Um job de envio de emails ou de cálculo de comissões precisa receber o tenant_id explicitamente e aplicá-lo em cada consulta. Quando o job usa a conexão privilegiada e processa uma fila compartilhada, um erro de parâmetro mistura clientes.

Índices de busca, como Elasticsearch, OpenSearch ou bancos vetoriais usados por recursos de IA, ficam fora do RLS do banco principal. O filtro por cliente precisa estar em toda consulta ao índice, aplicado no servidor. Um campo de autocompletar que sugere nomes de empresas de outros clientes é um sinal claro de índice sem filtro.

Como testar com duas contas

O teste mais eficiente para isolamento é simples de descrever. Crie duas empresas, A e B, cada uma com um usuário administrador e um usuário comum, e preencha as duas com dados reconhecíveis.

  • Logado em A, registre as requisições de cada tela e repita todas trocando os identificadores pelos de B: registros, arquivos, relatórios, exportações.
  • Envie tenant_id, company_id ou campos equivalentes no corpo, na query e em cabeçalhos, e confirme que o servidor ignora esses valores.
  • Gere relatórios e exportações nas duas contas e compare totais e linhas.
  • Baixe arquivos de B com a sessão de A, pela rota da aplicação e pela URL direta do bucket.
  • Dispare jobs em A e confira se emails, notificações e webhooks saem apenas com dados de A.
  • Busque na conta A termos que só existem em B, inclusive no autocompletar e em recursos de IA.
  • Repita os testes com o usuário comum para cobrir também a separação de perfis dentro do mesmo cliente.

O que a LGPD espera de quem hospeda os dados

Quando um SaaS trata dados pessoais em nome das empresas clientes, em regra ele atua como operador, e o cliente como controlador (art. 5º, VI e VII). O art. 39 determina que o operador trate os dados segundo as instruções do controlador. O art. 37 exige que controlador e operador mantenham registro das operações de tratamento que realizarem.

O art. 46 obriga os agentes de tratamento, o que inclui o operador, a adotar medidas de segurança técnicas e administrativas para proteger os dados de acessos não autorizados. Um vazamento entre clientes é exatamente um acesso não autorizado. Pelo art. 42, controlador ou operador que causar dano em violação à legislação de proteção de dados deve repará-lo, e o § 1º, I, estabelece que o operador responde solidariamente quando descumprir as obrigações da lei ou não seguir as instruções lícitas do controlador.

A comunicação do incidente à ANPD e aos titulares cabe ao controlador (art. 48). Por isso convém que o contrato com cada cliente defina como e em que prazo o SaaS avisará sobre um incidente, para que o cliente consiga cumprir essa obrigação. A análise do papel de cada parte depende do caso concreto e deve envolver o jurídico.

Checklist de isolamento

Os itens abaixo resumem os pontos de verificação mais comuns. A Tox faz esse tipo de revisão em sistemas SaaS combinando leitura de código, análise das políticas do banco e testes com contas de clientes distintos.

  • O tenant_id vem sempre da sessão, nunca da requisição.
  • Toda rota que recebe um identificador confere se o registro pertence ao cliente e ao usuário.
  • RLS ativo nas tabelas com dados de cliente, com FORCE ROW LEVEL SECURITY ou com a aplicação conectando por uma role que não é dona das tabelas.
  • Nenhuma role da aplicação com BYPASSRLS; credenciais privilegiadas inventariadas e restritas a rotas revisadas.
  • Buckets privados, caminhos com prefixo do cliente e links assinados de curta duração.
  • Relatórios, exportações e dashboards com filtro de tenant em todas as consultas e JOINs.
  • Chaves de cache e índices de busca incluem o cliente.
  • Jobs recebem o tenant_id explicitamente e não herdam contexto de outra execução.
  • Testes automatizados com duas contas rodando a cada deploy.
  • Contratos com clientes tratam instruções de tratamento, registro de operações e aviso de incidentes.

Fontes

Serviços relacionados

Este artigo tem caráter informativo e não substitui a leitura das normas citadas nem orientação jurídica. Veja outros artigos.

Fale com a Tox

Conte o que precisa. Respondemos com uma proposta de escopo, prazo e preço.

Solicitar proposta