Início › Blog › Isolamento multi-cliente

Isolamento multi-cliente: como o IDOR vaza dados entre clientes do seu SaaS

12 de outubro de 2026 · ~9 min de leitura

Quase todo SaaS é multi-tenant: todos os seus clientes vivem no mesmo banco, separados por um tenant_id (ou user_id, org_id). O isolamento entre eles depende de uma verificação em cada acesso. Esquecer uma única é a falha nº 1 do OWASP API Top 10 — o IDOR / BOLA — e é a que mais transforma um SaaS novo em notícia ruim. Este é o guia da falha: por que acontece, os 7 padrões e onde a checagem tem que ficar.

Por que é tão perigoso: não é um cliente vendo os dados dele — é um cliente vendo os de todos os outros. Um endpoint sem a checagem de dono, e qualquer cliente logado itera IDs e baixa a base inteira. Sem exploit nenhum: só trocar um número na URL.

IDOR / BOLA em uma frase

IDOR (Insecure Direct Object Reference) e BOLA (Broken Object Level Authorization) são o mesmo problema: a app usa um identificador vindo do cliente para buscar um objeto, mas não confirma que aquele objeto pertence a quem pediu. Autenticação (quem é você) está certa; autorização no nível do objeto (você pode ver este?) está faltando.

1. Autorizar por "logado", não por "é dono"

O padrão clássico. Passou pela autenticação, então lê qualquer objeto:

app.get('/invoices/:id', auth, async (req, res) => {
  const inv = await db.invoices.findById(req.params.id);
  res.json(inv); // ❌ nunca checa inv.tenantId
});

// ✅ a busca já é limitada ao tenant da sessão
const inv = await db.invoices.findOne({
  id: req.params.id, tenantId: req.session.tenantId
});
if (!inv) return res.sendStatus(404);

2. Confiar no tenant_id que o cliente manda

Se o tenant vem de um parâmetro, header ou corpo da requisição, o cliente simplesmente escolhe outro e atravessa a fronteira. O tenant tem que vir da sessão/token do servidor, nunca da requisição.

// ❌ cliente escolhe o tenant
const tid = req.query.tenantId || req.body.tenantId;
// ✅ tenant vem do token verificado
const tid = req.session.tenantId;

3. Achar que ID aleatório (UUID) é segurança

Trocar IDs sequenciais por UUID dificulta adivinhar, mas não é autorização. IDs vazam em URLs, logs, e-mails, referrers e respostas de API — o atacante raramente precisa adivinhar. UUID é defesa em profundidade; a checagem de dono continua obrigatória.

4. Esquecer o escopo nas listagens

Pior que vazar um objeto é vazar a coleção. Uma rota de listagem sem o filtro de tenant devolve a base toda de uma vez.

// ❌ lista tudo de todos
const rows = await db.orders.find({});
// ✅ escopo obrigatório
const rows = await db.orders.find({ tenantId: req.session.tenantId });

5. Recurso aninhado sem re-checar a cadeia

Em /projects/:pid/tasks/:tid é comum checar o projeto e confiar na tarefa — aí alguém passa um tid de outro projeto/tenant. Valide a cadeia inteira: a tarefa pertence ao projeto, e o projeto pertence ao tenant.

const task = await db.tasks.findOne({
  id: tid, projectId: pid
});
if (!task || project.tenantId !== req.session.tenantId) return res.sendStatus(404);

6. Cache, storage e arquivos sem tenant na chave

O isolamento não é só do banco. Uma chave de cache user:settings sem o tenant serve os dados do cliente errado; um caminho de arquivo /uploads/fatura.pdf sem o tenant deixa um baixar o do outro. Coloque o tenant na chave de tudo que é compartilhado.

cache.get('settings')            // ❌ compartilhado entre tenants
cache.get(`t:${tid}:settings`)   // ✅ isolado por tenant

7. Jobs, exportações e webhooks sem escopo

Relatórios agendados, exportações de CSV, reprocessamentos e handlers de webhook rodam fora do contexto da requisição — e é fácil esquecerem o tenant, misturando dados de clientes num mesmo arquivo ou e-mail. Todo processo em segundo plano tem que carregar e respeitar o tenant explicitamente.

Onde a checagem tem que ficar

Defesa em profundidade: aplique o isolamento na camada de dados — RLS do Supabase ou regras do Firebase, que barram mesmo se o código esquecer — e na camada de aplicação (o escopo por tenant em toda query). Uma não substitui a outra: a RLS é a rede de segurança; a checagem na app é a porta.

Como testar hoje: crie dois tenants (A e B). Logado como A, capture os IDs dos seus objetos. Agora, logado como B, tente ler/alterar cada um — pela URL, pela API, por tudo. Qualquer resposta que não seja 403/404 é um vazamento. Vale automatizar isso num teste de integração que roda no CI. Por que ferramentas de padrão não pegam isso: veja SAST e o que o Semgrep deixa passar.

Tem certeza de que um cliente não vê o outro?

O SAAS SHIELD revisa seu repositório procurando exatamente isso — autorização por objeto, escopo de tenant e RLS/regras — e devolve cada falha com local, evidência e correção.

Solicitar auditoria →