Bugs de autorização em apps CRUD: audite papéis, tenants e rotas
Aprenda a identificar e corrigir bugs de autorização em apps CRUD auditando checagens de papel, escopo por tenant e rotas de API antes que usuários acessem dados de outros.

Por que usuários acabam vendo os dados uns dos outros
Um bug de autorização acontece quando seu app permite que a pessoa errada acesse algo mesmo estando autenticada. Isso é diferente de autenticação, que é só provar quem alguém é (login, senha, magic link). Autorização responde outra pergunta: agora que você está logado, o que pode fazer e quais registros são seus?
Quando a autorização está mesmo levemente errada, o resultado raramente é pequeno. Um usuário pode ver faturas de outro cliente, editar o perfil de outra pessoa ou apagar registros que nem deveria conhecer. Em apps CRUD isso é especialmente perigoso porque a interface pode parecer normal até alguém mudar uma URL, ajustar uma requisição no navegador ou chamar uma rota de API sem proteção.
Isso aparece muito em dashboards, painéis de admin e portais de clientes. Esses apps crescem rápido: novas telas, novos endpoints, novos filtros. Uma falta de filtro de tenant numa query de tabela e, de repente, aparecem registros de outra conta.
Um padrão comum por trás desses bugs é o acúmulo de privilégios (privilege creep). Permissões se expandem silenciosamente com o tempo. Você começa com “usuários veem só suas coisas”, depois adiciona ferramentas de suporte, depois uma visão de admin, depois um endpoint de exportação. Cada passo adiciona um novo lugar onde a checagem pode falhar.
Um exemplo realista: um portal do cliente tem um botão “Baixar recibo”. O endpoint recebe um ID de recibo. Se o servidor checa apenas “usuário está logado” mas não “recibo pertence a este tenant”, um usuário pode adivinhar ou reutilizar um ID e baixar o recibo de outra pessoa.
Esses bugs sobrevivem porque falta uma das bases:
- uma regra clara de propriedade (usuário, equipe ou tenant)
- checagens consistentes em toda leitura e escrita
- testes que tentem intencionalmente acesso entre contas
Times costumam esbarrar nisso com protótipos gerados por IA. A autenticação funciona, mas as regras de autorização não são aplicadas de forma consistente entre rotas e queries. É aí que “funcionou na demo” vira vazamento de dados em produção.
Papéis, permissões e tenants — um modelo simples
A maioria dos vazamentos acontece porque o app não concorda numa pergunta simples: quem é este usuário e o que ele pode fazer agora?
Comece nomeando as peças de identidade que seu app deve carregar em cada requisição:
- User ID: a pessoa que faz a requisição.
- Role: o tipo de acesso que ela tem (viewer, editor, admin).
- Tenant ID: a organização/workspace/projeto a que pertence.
Uma checagem de papel responde “Você pode fazer essa ação?”. O escopo por tenant responde “Quais registros você pode atuar?”. Normalmente você precisa dos dois.
Por exemplo, role = editor pode permitir “editar faturas”. Mas sem escopo por tenant, uma query como “update invoice by id” pode atualizar uma fatura em outro workspace. O usuário tinha permissão, só não para aquele tenant.
Checagens de papel vs escopo por tenant
Checagens de papel costumam ser simples: este usuário pode deletar, pode convidar colegas, pode acessar a tela de cobrança. O escopo por tenant é a guarda que mantém toda leitura e escrita dentro da organização/workspace certo.
Uma forma rápida de lembrar:
- Papel decide o que você pode fazer.
- Tenant decide onde você pode fazer.
A armadilha do “admin”
“Admin” é onde muitos apps ficam relaxados. Há uma grande diferença entre:
- Admin de tenant: pode gerenciar usuários e configurações dentro do próprio workspace.
- Admin global: pode acessar tudo em todos os tenants (raro, normalmente equipe interna).
Se seu código tratar qualquer admin como global, um admin normal de cliente pode acabar vendo dados de outros clientes. Isso é comum quando “admin” é implementado como um único flag sem esclarecer o escopo.
Mais um aviso: restrições apenas na UI não são segurança. Esconder botões ajuda na usabilidade, mas não protege dados. Se uma rota de API aceita uma requisição, um usuário ainda pode chamá‑la diretamente, mesmo que a interface nunca mostre a opção.
Onde a autorização deve ser aplicada
Autorização não é uma checagem que você adiciona “em algum lugar no backend”. Na maior parte dos vazamentos, o time até tinha uma checagem, mas ela vivia no lugar errado ou só cobria uma camada.
Pense na aplicação das checagens como uma pilha. Cada camada bloqueia um tipo diferente de erro, especialmente em código construído rápido onde handlers são copiados e editados.
Pense em camadas (não em um único guarda)
Comece na borda, onde as requisições entram no sistema. Se um endpoint não deveria ser chamado por um papel, bloqueie antes de qualquer trabalho. Isso previne problemas do tipo “posso acessar a URL direto”.
Em seguida, aplique acesso ao registro específico. Não basta saber que alguém está logado. Você deve provar que o registro pertence ao tenant dele (ou que ele tem uma razão válida para cruzar tenants, como um papel de suporte interno).
Por fim, controle o que podem ver ou alterar dentro desse registro. Muitos apps limitam quais linhas são retornadas, mas ainda vazam campos sensíveis (notas internas) ou aceitam alterações em campos controlados pelo servidor (role, plan, tenantId).
Uma forma prática de posicionar checagens:
- Acesso à rota: quem pode chamar este endpoint
- Acesso ao registro: quais itens específicos podem ser lidos ou modificados
- Acesso à ação: o que pode ser feito (ler vs editar vs deletar vs exportar)
- Acesso a campos: quais propriedades são retornadas ou aceitas
Uma regra que pega a maioria dos vazamentos
Assuma que os clientes não são confiáveis, até mesmo seu frontend. Se uma requisição inclui um ID, um tenantId ou um role, trate como uma dica, não como fato. O servidor deve derivar identidade e tenant da sessão ou do token, e então aplicar isso em toda query e escrita.
Ao revisar código, procure lugares onde só existe uma camada. Se qualquer rota retorna dados, pergunte: aplicamos checagens em rota, registro, ação e campo, ou fizemos só uma e esperamos que cubra o resto?
Comece com um mapa de permissões que você consiga seguir
A maioria dos bugs de autorização começa como um problema de documentação. Ninguém consegue responder, num lugar só, “Quem pode fazer o quê, sobre quais registros?”. Então as checagens são adicionadas ad hoc, as rotas se desgastam e o privilégio cresce devagar.
Um mapa de permissões é uma referência em linguagem simples que você pode manter aberta enquanto audita rotas e queries. Mantenha‑o pequeno o suficiente para que um colega não técnico possa ler e identificar uma regra estranha.
1) Escreva a tabela papel→ações (sem termos de código)
Comece com os papéis que você realmente tem em produção, não os que planeja ter. Depois mapeie para ações usando verbos simples: ver, criar, editar, deletar, convidar, exportar, alterar cobrança.
| Role | Pode ver | Pode criar | Pode editar | Pode deletar | Pode gerenciar usuários |
|---|---|---|---|---|---|
| Member | Itens próprios | Sim | Itens próprios | Não | Não |
| Manager | Itens da org | Sim | Itens da org | Limitado | Convidar membros |
| Admin | Itens da org | Sim | Itens da org | Sim | Total |
Se você não consegue descrever uma regra sem exceções, é sinal de que precisa de mais um papel ou de um conceito como “owner”.
2) Marque cada fronteira de tenant e a chave que a define
Muitos apps têm mais de uma fronteira: account, org, workspace, project. Anote cada uma e escolha a chave de escopo (por exemplo, org_id ou workspace_id). Depois liste cada recurso e a chave de tenant que deve sempre estar presente.
Ao fazer isso, foque em três perguntas:
- O que escopa cada recurso (org_id, workspace_id, project_id)?
- De onde vem esse escopo (sessão, token, URL, corpo da requisição)?
- Quais fronteiras nunca podem ser cruzadas?
Finalmente, defina “owner” por recurso. Owner não é universal. Um comentário pode ser de quem o criou, uma tarefa do responsável, uma fatura da conta.
Um exemplo concreto: se “owner” de um documento significa “criador”, então um manager não deve editar automaticamente todo documento, a menos que sua tabela diga que managers podem editar todos os documentos da org. Esse detalhe evita um erro comum: usar checagens de papel para pular escopo por tenant.
Auditoria passo a passo: rotas de API e handlers do servidor
Bugs de autorização frequentemente se escondem em lugares chatos: rotas que você esqueceu que existem, o handler que “só atualiza um registro”, ou o endpoint de admin que nunca recebeu checagens reais. Uma auditoria é basicamente inventário mais disciplina.
1) Faça inventário de todas as rotas (sim, todas)
Liste todas as rotas de API que seu app expõe, incluindo internas, de admin e endpoints “temporários” adicionados durante prototipagem. Essas experiências muitas vezes ficam por aí e continuam acessíveis.
Escolha uma fonte de verdade (seu arquivo de rotas, pasta de rotas do framework ou configuração do gateway de API) e crie uma tabela simples. Se encontrar rotas não usadas, marque para remoção — mas audite antes.
2) Para cada rota, escreva: ator, ação, recurso, fronteira de tenant
Para cada rota, escreva uma frase em inglês simples:
- Ator: quem chama (usuário logado, admin da org, job do sistema)?
- Ação: o que fazem (ler, criar, atualizar, deletar)?
- Recurso: qual objeto é tocado (invoice, project, user, file)?
- Fronteira de tenant: qual container precisa bater (org_id, workspace_id, account_id)?
Se você não consegue descrever a regra em uma frase, o código geralmente está inconsistente.
3) Verifique escopo de tenant no servidor, não no cliente
Cheque que o handler deriva o tenant da sessão autenticada (ou das claims do token do servidor), não de campos do corpo da requisição ou query params.
Um sinal de alerta comum: a requisição inclui orgId e o servidor confia nele. Um padrão mais seguro é: leia org_id da sessão do usuário e então aplique isso em cada query e mutação.
4) Confirme que escritas também são escopadas (não só leituras)
Times costumam escopar páginas de listagem mas esquecer updates e deletes. Procure endpoints como:
PATCH /projects/:idDELETE /invoices/:idPOST /members/:id/role
Se o handler atualiza apenas por id, há risco instantâneo de cross‑tenant. A checagem deve ser: “registro com este id E este tenant pertence a este ator”.
5) Fique de olho em rotas que aceitam IDs e buscam registros “nus”
Qualquer rota que receba um id é um ponto sensível. O padrão perigoso é:
- buscar por
id - checar algo de forma frouxa (ou não checar)
- retornar ou mutar
Em vez disso, aplique autorização como parte da busca. Se o registro não estiver no tenant do ator (ou o ator não tiver permissão), a busca deve falhar.
Auditoria passo a passo: queries de banco e filtros do ORM
Autorização pode parecer correta no controller e depois quebrar silenciosamente na camada de banco. Se uma query pode retornar registros de outro tenant, o app vai eventualmente mostrá‑los, talvez em busca, exports ou casos de borda que você não testou.
Comece encontrando cada lugar em que seu app lê “muitas linhas” (páginas de listagem, busca, tabelas de admin, jobs em background). Para cada query, pergunte uma coisa: onde o filtro de tenant é aplicado, e ele pode ser pulado?
1) Audite queries de listagem (muitas linhas)
Abra cada endpoint de listagem e trace até a chamada do ORM. Restrições de tenant devem fazer parte da query toda vez, não ser adicionadas depois em memória.
Um checklist que pega a maioria dos vazamentos:
- O escopo de tenant está na query do banco (não filtrado após o fetch).
- Paginação usa a mesma query escopada (queries de count e dados batem).
- Termos de busca são combinados com o escopo do tenant usando AND, não OR.
- Lógica de ordenação e cursor não pode cair para uma query base sem escopo.
- “Include related data” não carrega filhos de outro tenant.
2) Audite queries de detalhe (linha única)
Endpoints de detalhe nunca devem buscar apenas por id. A busca precisa incluir tenantId e id (ou outra chave única que esteja vinculada ao tenant). Se seu ORM tem helpers como findUnique(id), trate como suspeito a menos que a chave única inclua tenantId.
Prefira findFirst where tenantId = X and id = Y em vez de find by id then check tenant later. O segundo padrão é fácil de esquecer em um handler.
3) Joins, exports e queries “especiais”
Joins são um local comum onde o escopo some. Uma query pode começar escopada e depois fazer join em outra tabela e filtrar pela chave de tenant errada (ou não filtrar).
Também cheque relatórios, exports e jobs em background. Eles frequentemente burlam o código normal da API, então precisam das mesmas regras de escopo na query.
Armadilhas comuns que causam privilégio gradual
Privilege creep geralmente não acontece porque alguém escreve “permitir tudo”. Acontece porque atalhos pequenos se acumulam: uma rota confia na UI, outra assume que “admin” é global, uma terceira esquece de verificar um job em background.
Erro 1: Confiar no cliente (flags da UI, botões escondidos)
Se um usuário pode editar uma requisição, ele pode editar qualquer coisa que o navegador envie. Um botão “Deletar” escondido ou uma flag role: "admin" no cliente não é proteção. O servidor deve decidir com base na identidade logada.
Uma versão comum: a UI esconde “Editar fatura” a menos que você seja manager, mas a rota de API só checa que você está autenticado. Qualquer um pode chamar a rota direto e atualizar a fatura de outra pessoa.
Erro 2: Usar isAdmin sem escopo de tenant
“Admin” não tem sentido sem dizer: admin de quê? Em apps multi‑tenant, a maioria dos papéis deve ser escopada ao tenant. A armadilha é escrever lógica tipo “se isAdmin, permitir” e acidentalmente conceder acesso a todos os tenants.
Uma checagem mental mais segura: toda decisão de autorização deve responder duas perguntas, “quem é este usuário?” e “a que tenant pertencem estes dados?”. Se qualquer resposta for ambígua, você está a uma refatoração de acesso cross‑tenant.
Erro 3: Checar após carregar registro
Muitos vazamentos acontecem porque o código busca o registro primeiro e só depois verifica se o usuário pode vê‑lo. Mesmo se bloquear a resposta, você ainda pode vazar por mensagens de erro diferentes, timing ou dados relacionados carregados junto.
Prefira checagens que impeçam o registro de ser buscado incluindo regras de tenant e propriedade diretamente na query.
Erro 4: Esquecer “portas alternativas” (jobs, webhooks, downloads)
Jobs em background, tarefas agendadas, handlers de webhook e endpoints de download de arquivos frequentemente pulam o middleware usual e acabam com checagens mais fracas. Se um job processa “todas as faturas” sem filtro de tenant, pode enviar por e‑mail ou exportar dados de cliente errado.
Para esses caminhos, responda: autentica o chamador (ou valida o webhook)? aplica escopo de tenant em cada query? registra o que tocou (tenant id, record id, ator)?
Erro 5: Helpers compartilhados com defaults duvidosos
Um helper como getUserProjects(userId) parece seguro até alguém reutilizá‑lo numa tela de admin e assumir que retorna “todos os projetos”. Ou pior, um helper pode defaultar para “sem filtro de tenant” quando tenantId está ausente.
Bons helpers falham alto (fail loudly). Se tenantId é necessário por segurança, torne‑o obrigatório na assinatura da função e lance erro se faltar.
Um exemplo realista: uma rota ruim, um grande vazamento
Imagine que há um papel Support Agent. Ele deve ver tickets de suporte, mas só da própria organização (tenant). Parece simples, mas basta um endpoint descuidado para quebrar a regra.
Erro: a API tem GET /api/tickets/:ticketId. O handler checa que o usuário está logado e busca o ticket por ID. Nunca verifica tenant.
// Unsafe: fetches by ID only
const ticket = await db.ticket.findUnique({
where: { id: ticketId }
});
return ticket;
Por que isso vaza dados: IDs de ticket aparecem em lugares acessíveis, como URLs no navegador, notificações por e‑mail, logs de ferramentas de suporte ou CSVs exportados. Mesmo sem isso, muitos apps usam IDs previsíveis (números incrementais, UUIDs curtos copiados da UI). Um usuário curioso ou malicioso pode trocar um ID e ver um ticket de outra org.
Esse é um dos fracassos mais comuns: o código assume que conhecer um ID é prova de que você deve ver o registro.
Um handler mais seguro faz duas coisas diferentes:
- Escopa a query ao tenant (org) da sessão.
- Checa o papel para a ação (visualizar ticket).
// Safer: enforce role + tenant scoping
if (user.role !== "support_agent") throw new Error("Forbidden");
const ticket = await db.ticket.findFirst({
where: { id: ticketId, orgId: user.orgId }
});
if (!ticket) throw new Error("Not found");
return ticket;
Note o comportamento “Not found”. Evita confirmar que um ticket existe em outra org.
Para verificar a correção, mantenha o teste simples:
- Crie duas orgs, Org A e Org B.
- Crie um ticket na Org B.
- Faça login como Support Agent na Org A.
- Chame o endpoint com o
ticketIdda Org B. - Confirme que recebe “Not found” (ou 404) e nenhum dado do ticket.
Checagens rápidas que você pode rodar antes de um release
A maioria dos bugs de autorização aparece no último trecho: uma nova rota, um atalho de admin “útil” ou uma query que esqueceu escopo. Essas checagens são simples e repetíveis.
Teste rápido com duas contas (10 minutos)
Crie duas contas de teste que pareçam normais mas pertençam a tenants diferentes (Empresa A e Empresa B). Dê‑lhes dados realistas para que você reconheça o que pertence a cada uma.
Então misture intencionalmente identificadores:
- Copie um ID de registro do Tenant A e tente lê‑lo a partir do Tenant B.
- Tente atualizar usando o ID do Tenant A enquanto logado como Tenant B.
- Tente deletar usando o ID do Tenant A enquanto logado como Tenant B.
- Se seu app usa soft delete, teste restaurar/undelete também.
- Repita para objetos filhos (comentários, faturas, arquivos) que podem ter escopos diferentes.
Se qualquer um desses funcionar ou retornar dados reais, provavelmente há filtros de tenant faltando ou uma checagem de papel que só roda na UI.
Não esqueça features em massa e portas alternativas
Vazamentos frequentemente acontecem fora das telas CRUD principais. Um endpoint de listagem pode estar escopado, mas o export não. Um download de arquivo pode pular checagens por ser “só uma URL”.
Faça uma revisão rápida em:
- Páginas de listagem com filtros, busca, ordenação e paginação (tente buscar um valor conhecido do outro tenant).
- Endpoints de exportação (CSV, PDF, relatórios) e jobs em background que os geram.
- Downloads e previews de arquivos (signed URLs, IDs de anexos, endpoints de imagens).
- Logs de atividade, dashboards de admin e widgets de “itens recentes”.
- Qualquer rota que aceite um ID no path, mesmo se a UI nunca o expõe.
Confirme também que seus papéis de admin têm o escopo pretendido. “Admin de tenant” não deve agir como “admin global” só por conveniência.
Próximos passos: torne a autorização difícil de quebrar
Falhas de autorização raramente acontecem porque as pessoas não se importam. Acontecem porque checagens estão espalhadas, filtros de tenant são fáceis de esquecer e novas funcionalidades são lançadas mais rápido do que as regras são atualizadas. O objetivo é fazer o caminho seguro ser o mais fácil.
Coloque um guardrail único na frente de tudo
Use uma camada de autorização consistente em todos os lugares: middleware, um helper de policy ou um serviço que todo handler chame antes de trabalhar. Se você precisa lembrar quais rotas “precisam de checagem”, vai esquecer alguma.
Uma boa regra prática: handlers de rota não deveriam conter lógica de permissão customizada. Eles devem perguntar à camada de políticas uma pergunta clara (por exemplo, “Este usuário pode atualizar esta fatura?”) e então prosseguir.
Mudanças que reduzem erros rapidamente:
- Crie um helper/policy por recurso: read, create, update, delete.
- Faça do escopo por tenant o padrão (por exemplo, um helper de query que sempre aplica tenantId).
- Negue por padrão quando dados estiverem faltando ou ambíguos (sem tenant, sem papel, sem ownership).
- Logue negações de autorização com contexto suficiente para depurar (usuário, tenant, recurso, ação).
Embuta o escopo por tenant no acesso a dados
Centralize o escopo por tenant para que seja difícil esquecer. O melhor lugar é onde as queries são construídas, não onde as respostas são retornadas.
Por exemplo, em vez de escrever where: { id } em muitos lugares, exponha um helper que já inclua tenantId. Se um desenvolvedor tentar burlar, isso deve parecer errado na revisão de código.
Testes de alto valor pegam as regressões que mais importam:
- Leitura cross‑tenant falha (Usuário A não consegue buscar registro do Usuário B por ID).
- Escrita cross‑tenant falha (Usuário A não consegue atualizar/deletar registro do Usuário B).
- Rebaixamento de papel é seguro (um usuário que perde admin não mantém acesso de admin).
- Criação é escopada (novos registros recebem o tenant atual).
Se você herdou um codebase gerado por IA e não confia que o escopo por tenant e as checagens de papel são aplicadas consistentemente, uma auditoria focada pode poupar dias de tentativa e erro. FixMyMess (fixmymess.ai) é especialista em diagnosticar e reparar esse tipo de lacuna de autorização, especialmente os handlers “apenas‑id” e queries sem escopo que parecem corretos até o usuário real chegar em produção.
Perguntas Frequentes
Por que usuários veem dados de outra pessoa mesmo com login funcionando?
É um problema de autorização, não de autenticação. O usuário pode estar totalmente autenticado, mas o servidor não verifica consistentemente que o registro solicitado pertence ao seu usuário/equipe/tenant antes de devolvê‑lo.
Qual é a diferença entre autenticação e autorização?
Autenticação responde “quem é você?”. Autorização responde “o que você pode fazer e quais registros são seus?”. A maioria dos vazamentos entre contas acontece quando o app só verifica que o usuário está logado e então busca dados por ID sem checar propriedade ou escopo do tenant.
Preciso de checagens de papel, escopo por tenant, ou ambos?
As checagens de papel (role) determinam que ações o usuário pode executar, por exemplo “pode editar faturas”. O escopo por tenant determina onde ele pode executar essas ações, por exemplo “apenas dentro deste workspace”. Normalmente você precisa dos dois: o usuário pode ter o papel certo, mas mirar os registros do tenant errado.
Quais endpoints têm maior probabilidade de vazar dados entre tenants?
Qualquer endpoint que aceite um ID é um ponto crítico, especialmente downloads, exports e rotas de detalhe. Se o handler busca um registro apenas por id, um usuário pode trocar IDs e potencialmente acessar dados de outro tenant.
Se a UI esconde recursos de admin, isso basta como segurança?
Não. Esconder botões melhora a usabilidade, mas não protege a API. Assuma que qualquer pessoa pode chamar seus endpoints diretamente; o servidor deve aplicar permissões e propriedade do tenant em toda leitura e escrita.
O que é a “armadilha do admin” e como evitar?
“Admin” precisa ter escopo. Um admin de tenant só deve gerenciar dentro do seu tenant; um admin global vê tudo e deve ser raro e bem controlado. Se o código tratar qualquer admin como global, você pode conceder acesso entre tenants acidentalmente.
Por que é arriscado carregar um registro primeiro e checar acesso depois?
Porque é fácil esquecer um handler, e você ainda pode vazar informações por efeitos laterais como diferenças de erro (404 vs 403), tempos de resposta, ou dados relacionados carregados paralelamente. O padrão mais seguro é incluir regras de tenant e propriedade diretamente na consulta ao banco, para que registros não autorizados nem sejam buscados.
O servidor deve confiar em tenantId ou role enviados pelo cliente?
Derive identidade e tenant das claims do token ou da sessão no servidor, e então aplique‑as em cada query e mutação. Trate qualquer tenantId, role ou userId vindo do cliente como uma dica, no máximo, e nunca como fonte de verdade.
Qual é a maneira mais rápida de testar fumegação (smoke test) por vazamentos de autorização?
Crie duas contas em tenants diferentes com dados distintos. Enquanto estiver logado como Tenant B, tente ler, atualizar e deletar registros de Tenant A reaproveitando IDs; se algo retornar dados reais ou permitir a ação, há uma falha de escopo que precisa ser corrigida antes do release.
Por que apps CRUD gerados por IA têm tantos bugs de autorização, e o que posso fazer?
Prototótipos gerados por IA costumam deixar autenticação funcionando rápido, mas aplicam autorização de forma inconsistente em rotas e queries, especialmente em handlers copiados e endpoints temporários. Se você herdou um codebase assim e quer deixá‑lo seguro para produção rápido, FixMyMess (fixmymess.ai) pode fazer uma auditoria e reparar handlers que usam só ID, queries sem escopo e erros de papel/tenant, muitas vezes em 48–72 horas após uma auditoria de código gratuita.