Aikido

Da detecção à prevenção: Como o Zen impede vulnerabilidades IDOR em tempo de execução

Escrito por
Hans Ott

TL:DR

IDORs são a principal forma como empresas SaaS multi-tenant vazam dados, e geralmente são descobertas após a implantação. Aikido Zen torna o isolamento de tenants não opcional. O Zen analisa todas as suas consultas SQL em tempo de execução usando um parser SQL adequado (escrito em Rust), verifica se cada consulta filtra o tenant correto e gera um erro caso contrário. Os desenvolvedores não podem mais implantar acidentalmente acesso entre tenants. Está disponível hoje para Node.js, com Python, PHP, Go, Ruby, Java e .NET em breve.

Por que os IDORs estão mais perigosos agora

As vulnerabilidades IDOR, também conhecidas como Insecure Direct Object References, são uma das falhas mais comuns e perigosas em aplicações multi-tenant. Elas ocorrem quando uma consulta esquece de filtrar por tenant, permitindo que uma conta acesse os dados de outra.

Por muito tempo, os IDORs eram difíceis de detectar. Eles não apareciam em varreduras de código e exigiam muito esforço manual. Por causa disso, muitos bugs de IDOR só eram descobertos durante pentests caros e trabalhosos, ou quando pesquisadores de segurança os encontravam através de programas de bug bounty.

Mas isso mudou. Ferramentas de teste de segurança agentic agora podem se comportar como usuários reais, clicando em fluxos de trabalho, trocando de papéis e tentando acessar recursos automaticamente. Isso torna as vulnerabilidades IDOR muito mais fáceis de detectar. Mas isso é uma faca de dois gumes: Se essas falhas são mais fáceis de encontrar, elas também são mais fáceis de explorar. É por isso que as organizações não devem focar apenas na detecção de IDORs, mas na prevenção.

Por que a detecção não é suficiente

No lado da detecção, o AI Pentest da Aikido já consegue encontrar vulnerabilidades IDOR, algo que as ferramentas SAST tradicionais baseadas em padrões nunca conseguiriam fazer de forma confiável, pois o IDOR exige a compreensão do contexto de autorização, e não apenas padrões de código. O AI Pentest autentica-se como usuários reais, executa fluxos de trabalho completos e reutiliza identificadores de objeto entre diferentes funções para encontrar falhas IDOR exploráveis reais. É por essa razão que muitas organizações que utilizam nossa capacidade de AI Pentest estão predominantemente interessadas em encontrar IDORs.

Mas encontrar vulnerabilidades IDOR é apenas metade da equação. Em um mundo ideal, você as impediria de serem introduzidas em primeiro lugar. É sobre isso que este post trata: proteção IDOR no Zen, nosso firewall incorporado no aplicativo de código aberto. Ele analisa cada consulta SQL em tempo de execução e gera um erro se uma consulta estiver sem um filtro de tenant ou usar o ID de tenant errado, capturando o bug durante o desenvolvimento e os testes antes que ele chegue à produção.

Em muitos ambientes corporativos, especialmente durante revisões de segurança ou avaliações de fornecedores, uma pergunta recorrente é como a multi-tenancy é imposta e como o acesso a dados entre tenants é prevenido.

Equipes de segurança e lideranças desejam garantias técnicas claras de que os limites dos tenants são impostos sistematicamente, e não apenas por convenção.

Ter um mecanismo que valida automaticamente o escopo do tenant no nível da consulta fornece uma resposta direta e crível. Isso move a conversa de "contamos com os desenvolvedores para lembrar disso" para "o sistema impõe isso automaticamente".

Veja como a configuração se parece:

import Zen from "@aikidosec/firewall";

// 1. Tell Zen which column identifies the tenant
Zen.enableIdorProtection({
  tenantColumnName: "tenant_id",
  excludedTables: ["users"],
});

// 2. Set the tenant ID per request (e.g., in middleware after authentication)
app.use((req, res, next) => {
  Zen.setTenantId(req.user.organizationId);
  next();
});

// 3. Optionally bypass for specific queries (e.g., admin dashboards)
const result = await Zen.withoutIdorProtection(async () => {
  return await db.query("SELECT count(*) FROM orders WHERE status = 'active'");
});

Como é uma vulnerabilidade IDOR?

Se seu aplicativo possui contas, organizações, workspaces ou equipes, você provavelmente tem uma coluna como tenant_id que mantém os dados de cada conta separados. Mas quando a consulta esquece de filtrar por essa coluna, ou filtra pelo valor errado, significa que uma conta pode acessar os dados de outra conta. Esta é uma vulnerabilidade IDOR.  

Aqui está um exemplo simples. Você tem um endpoint que retorna os pedidos de um usuário:

app.get("/orders/:orderId", async (req, res) => {
  const order = await db.query(
    "SELECT * FROM orders WHERE id = $1",
    [req.params.orderId]
  );

  res.json(order);
});

Vê o problema? Não há tenant_id filtro. Se Alice enviar GET /orders/42 e o pedido 42 pertence ao Bob, Alice obtém o pedido do Bob. Isso é um IDOR.

A correção é simples, adicione um WHERE tenant_id = $2 cláusula. No entanto, o bug é fácil de introduzir e difícil de detectar, especialmente em uma grande base de código com centenas de queries em dezenas de arquivos. Um filtro esquecido é tudo o que é preciso.

IDOR é uma categoria ampla. Também inclui coisas como acessar arquivos de outros usuários via manipulação de URL ou endpoints de API que não verificam a propriedade. Este post foca especificamente no subconjunto de filtragem de tenant em SQL, garantindo que cada query de banco de dados seja adequadamente delimitada ao tenant atual. Para um aprofundamento maior sobre IDOR de forma mais ampla, confira nosso IDOR vulnerabilities explained post.
O que existe hoje

Além da varredura de segurança, que se concentra mais em encontrar bugs existentes, existem outros métodos para evitar que novos IDORs sejam introduzidos: bibliotecas em nível de framework e aplicação em nível de banco de dados. Cada um tem pontos fortes e limitações.

Bibliotecas em nível de framework

Vários frameworks possuem bibliotecas que delimitam automaticamente as queries ao tenant atual:

  • Ruby on Rails: acts_as_tenant adiciona delimitação automática de tenant a modelos ActiveRecord. Declare acts_as_tenant(:account) e todas as queries nesse modelo são filtradas pelo tenant atual.
  • Django: django-multitenant faz o mesmo para o ORM do Django. Defina o tenant atual no middleware, e Product.objects.all() automaticamente se torna delimitado ao tenant.
  • Laravel: Tenancy for Laravel fornece multi-tenancy de banco de dados único e multi-banco de dados, com troca automática de contexto.
  • .NET / EF Core: Filtros de query globais permitem aplicar WHERE tenant_id = X a cada query automaticamente no nível do modelo.

Essas bibliotecas funcionam bem dentro de seus próprios ORMs. A limitação é que elas protegem apenas as queries que passam pela abstração do ORM. Queries SQL brutas, queries de outras bibliotecas ou queries construídas com um query builder diferente no mesmo projeto não serão delimitadas. Elas também são opt-in, você precisa se lembrar de adicionar a anotação a cada modelo, e novos modelos podem passar despercebidos. Para ser justo, acts_as_tenant tem um require_tenant config que levanta um erro quando nenhum tenant é definido, o que mitiga significativamente o risco de "esquecer de definir o tenant".

Existem também armadilhas sutis. No Rails, por exemplo, acts_as_tenant funciona adicionando um default_scope. Se um desenvolvedor chamar Project.unscoped para remover um escopo padrão diferente, como um arquivado filter, ele remove todos os escopos padrão, incluindo o filtro de tenant, sem erro ou aviso. O Rails possui unscope (sem o d) para remover cirurgicamente um único escopo, mas isso exige saber que o escopo de tenant existe em primeiro lugar. Em uma base de código com muitos desenvolvedores, alguém eventualmente recorrerá a unscoped, e o limite do tenant desaparece silenciosamente.

Imposição em nível de banco de dados

PostgreSQL Row-Level Security (RLS) leva isso um passo adiante, impondo o isolamento de tenant no nível do banco de dados. Em vez de depender da sua aplicação para adicionar WHERE tenant_id = ? a cada consulta, você instrui o próprio Postgres a aplicá-lo:

-- 1. Habilitar RLS em cada tabela
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 2. Criar uma política: permitir apenas linhas que correspondam à variável de sessão
CREATE POLICY tenant_isolation ON projects
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- 3. Por requisição, definir o contexto do tenant antes de executar as consultas
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';

-- Agora, mesmo um SELECT simples retorna apenas as linhas desse tenant
SELECT * FROM projects;

RLS é a garantia mais forte das abordagens listadas aqui. Consultas SQL puras ou ORM, não importa. O banco de dados a impõe. E, ao contrário de acts_as_tenant, se você esquecer de definir a variável de sessão, nenhum dado é retornado em vez de todos os dados. Esse é um padrão muito mais seguro.

Mas isso vem com desvantagens reais. O RLS não gera erros; as consultas retornam silenciosamente menos linhas ou não afetam nada. Isso é mais seguro do que retornar todos os dados, mas dificulta a depuração. Um UPDATE que deveria modificar 100 linhas pode afetar silenciosamente 0 devido a uma incompatibilidade de política, e é difícil distinguir isso de "nenhum dado existe".

O pooling de conexões também adiciona complexidade. RLS com SET não funciona corretamente com pgBouncer no modo de pooling de statement ou transaction, o que pode levar ao retorno de linhas para o tenant errado. Isso pode aparecer apenas em produção.

Existem também limitações estruturais. Superusuários ignoram todas as políticas completamente, e as views ignoram o RLS por padrão, então seu aplicativo deve se conectar como um papel de não-superusuário. Finalmente, é exclusivo do Postgres. Se você precisar dar suporte a MySQL, SQLite para desenvolvimento, ou outro armazenamento de dados, sua camada de segurança não o acompanha.

A conclusão pragmática: RLS é excelente como uma rede de segurança para o isolamento de tenant, mas a complexidade operacional e a dificuldade de depuração significam que não é uma solução plug-and-play para todas as equipes.

Onde o Zen se encaixa

Todas essas são abordagens válidas, e se você estiver usando uma delas, ótimo. A proteção IDOR do Zen é projetada para um cenário diferente: suas consultas passam por um driver de banco de dados, diretamente ou via um ORM, e você quer uma rede de segurança que funcione independentemente do ORM, query builder ou padrão SQL bruto que você use, sem alterar a configuração do seu banco de dados ou adotar uma biblioteca de framework específica.

O Zen tem suas próprias compensações, para ser honesto. Assim como acts_as_tenant, ele exige que você chame setTenantId em cada requisição. Se você esquecer, o Zen lança um erro, então ele falha ruidosamente em vez de silenciosamente, mas é a mesma classe de configuração por requisição. E, ao contrário do RLS, o Zen cobre apenas consultas que são executadas dentro da sua aplicação. Se alguém se conectar diretamente ao banco de dados, por exemplo, via psql ou um serviço separado sem o Zen, essas consultas não serão verificadas.

Ele também é agnóstico em relação à linguagem por design. Como o motor de análise SQL é escrito em Rust, nós o compilamos para WebAssembly para Node.js e Go, e para uma biblioteca nativa que outros agentes chamam via FFI. A proteção IDOR especificamente também estará disponível para os agentes Python, PHP, Go, Ruby, Java e .NET.

Como o Zen protege contra IDORs

O Zen reside dentro da sua aplicação e analisa consultas SQL em tempo de execução, com contexto completo sobre quem está fazendo a requisição.

Um parser SQL adequado, escrito em Rust

No coração da proteção IDOR do Zen está um parser SQL real construído sobre o crate sqlparser em Rust, compilado para WebAssembly para Node.js e Go. Ele analisa o SQL da mesma forma que um banco de dados faria, construindo uma Árvore de Sintaxe Abstrata (AST) completa da sua consulta, e então percorre a árvore para extrair:

  • Quais tabelas a consulta afeta (incluindo aliases)
  • Quais filtros de igualdade estão na cláusula WHERE
  • Quais colunas e valores estão nas declarações INSERT

Por que não regex? Regex funciona bem para consultas simples como SELECT * FROM orders WHERE tenant_id = ?. Mas aplicações do mundo real têm CTEs, UNIONs, subqueries, JOINs com aliases e todos os tipos de SQL válido com os quais uma abordagem baseada em regex tem dificuldade. À medida que as consultas se tornam mais complexas, a análise baseada em regex torna-se cada vez mais frágil. Não está necessariamente errada, mas é difícil de manter e fácil de ser surpreendido.

Um parser adequado lida com tudo isso de forma nativa. Ele também reconhece corretamente declarações que não precisam de verificação, como declarações DDL (CREATE TABLE, ALTER TABLE)BEGIN, COMMIT, ROLLBACK), controle de transação (SET, SHOW).

)

Veja como a análise funciona internamente. Dada esta consulta: SELECT * FROM orders
LEFT JOIN order_items ON orders.id = order_items.order_id
WHERE orders.tenant_id = $1
AND orders.status = 'active';

O parser produz:

[
  {
    "kind": "select",
    "tables": [
      { "name": "orders" },
      { "name": "order_items" }
    ],
    "filters": [
      { "table": "orders", "column": "tenant_id", "value": "$1" },
      { "table": "orders", "column": "status", "value": "active" }
    ]
  }
]

O Zen então verifica se cada tabela na query possui um filtro em tenant_id, e se o valor do filtro corresponde ao tenant atual.

O mesmo se aplica a INSERT, UPDATE, e DELETE. O Zen garante que a coluna de tenant esteja sempre presente e com o valor correto. Estes são lançados como erros, não apenas registrados. IDOR é um bug de desenvolvedor, não um ataque externo, então você quer que ele apareça ruidosamente durante o desenvolvimento e testes, em vez de passar despercebido para a produção.

Desempenho

Fazer o parsing de SQL em cada query parece custoso, mas na prática é rápido. A principal percepção é que a maioria das aplicações usa prepared statements ou parameterized queries. A string SQL permanece a mesma, apenas os valores dos parâmetros mudam. Assim, SELECT * FROM orders WHERE tenant_id = $1 AND status = $2 é analisado uma vez, e cada execução subsequente da mesma query é um acerto de cache.

Na primeira vez que o Zen encontra uma nova string de query, o parser Rust constrói a AST e extrai as tabelas e filtros. A partir daí, é apenas uma consulta de cache e uma comparação do ID do tenant com o valor do placeholder resolvido.

Se você estiver incorporando valores diretamente em strings SQL, por exemplo, concatenação de strings em vez de parameterized queries, cada string única exige um novo parsing. Mas você provavelmente não deveria estar fazendo isso de qualquer forma. Parameterized queries protegem contra SQL injection e, por acaso, também tornam a verificação de IDOR rápida.

O caminho para a produção: comendo nossa própria comida de cachorro

Nós implantamos a proteção IDOR do Aikido Zen em vários dos serviços internos do Aikido. Isso imediatamente revelou casos de borda que precisavam ser tratados.

O suporte a transações foi um bloqueador no início. Aplicações reais usam BEGIN, COMMIT, e ROLLBACK, e o Zen precisava reconhecer estas como declarações seguras que não exigem filtragem por tenant, em vez de gerar erros. Adicionamos isso rapidamente após vê-lo falhar em nossa primeira implantação interna.

Common Table Expressions (CTEs) foram outro desafio. Uma CTE como WITH active AS (SELECT * FROM orders WHERE tenant_id = $1) cria uma tabela virtual que as queries downstream referenciam. O Zen precisava rastrear os nomes das CTEs e excluí-los da lista de “tabelas reais”, enquanto ainda analisava o corpo da CTE para uma filtragem adequada.

O withoutIdorProtection escape hatch também se mostrou essencial. Nem toda query precisa de filtragem por tenant, como dashboards de administração, background jobs ou analytics cross-tenant. Inicialmente, tentamos uma ignoreNextQuery abordagem, onde você chamaria uma função antes da consulta para pular a verificação para a próxima instrução SQL:

Zen.ignoreNextQuery();
const result = await db.query("SELECT count(*) FROM orders");

Isso se mostrou frágil na prática. Com pools de conexão, a "próxima consulta" em uma dada conexão pode não ser aquela que você pretendia pular. A abordagem baseada em callback withoutIdorProtection é explícita quanto ao escopo. A proteção IDOR é desabilitada pela duração do callback e nada mais.

Como protegemos nossa API que serve dados de ativos da Cloud

Um dos serviços que protegemos desde o início foi a API interna que serve dados de ativos da Cloud em toda a plataforma.

Esta API é usada pela UI, por jobs em segundo plano e por vários motores de segurança sempre que precisam ler informações sobre a infraestrutura de um cliente. Ela está no centro do sistema e lida com milhares de requisições por segundo.

Como a plataforma é totalmente multi-tenant, o isolamento estrito de tenants é crítico. Cada consulta deve ser escopada para a organização correta, e não podemos depender de os desenvolvedores se lembrarem de adicionar o filtro certo em cada caminho de código.

Antes que o Zen suportasse a proteção IDOR nativamente, tínhamos uma implementação customizada que aplicava o escopo de tenant no nível da consulta. Assim que o Zen introduziu suporte de primeira classe para este comportamento, migramos da solução interna para a funcionalidade integrada, o que representa consideravelmente menos código para mantermos.

Hoje, o Zen verifica automaticamente se as consultas estão corretamente escopadas para o tenant atual, mesmo sob alta carga. Após a introdução da proteção IDOR do Zen, não notamos impacto significativo no desempenho.

Detecção e prevenção

O Aikido's AI Pentest encontra vulnerabilidades IDOR em sua aplicação em execução simulando ataques reais. A proteção IDOR do Zen impede que elas sejam introduzidas em primeiro lugar, detectando filtros de tenant ausentes durante o desenvolvimento.

Juntos, eles cobrem ambos os lados. O AI Pentest valida que seu código existente está seguro, e o Zen garante que o novo código permaneça seguro. Use o AI Pentest para auditar o que já está implantado. Use o Zen para detectar erros enquanto você os escreve.

Começando

A proteção IDOR está disponível hoje em @aikidosec/firewall para Node.js. Confira o guia de configuração para começar. O suporte para outras linguagens estará disponível em breve!

Compartilhar:

https://www.aikido.dev/blog/zen-stops-idor-vulnerabilities

Assine para receber notícias sobre ameaças.

4.7/5
Cansado de falsos positivos?

Experimente Aikido como 100 mil outros.
Começar Agora
Obtenha um tour personalizado

Confiado por mais de 100 mil equipes

Agende Agora
Escaneie seu aplicativo em busca de IDORs e caminhos de ataque reais

Confiado por mais de 100 mil equipes

Iniciar Escaneamento
Veja como o pentest de IA testa seu aplicativo

Confiado por mais de 100 mil equipes

Iniciar Testes

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.