Aikido

Da deteção à prevenção: como o Zen impede vulnerabilidades IDOR em tempo de execução

Escrito por
Hans Ott

TL:DR

Os IDORs são a principal forma pela qual as empresas de SaaS multitenant vazam dados, e geralmente são descobertos após a implementação. Aikido torna o isolamento de tenants obrigatório. O Zen analisa todas as suas consultas SQL em tempo de execução usando um analisador SQL adequado (escrito em Rust), verifica se todas as consultas filtram o tenant correto e gera um erro se isso não acontecer. Os programadores não podem mais enviar 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 são mais perigosos agora

As vulnerabilidades IDOR, também conhecidas como Referências Diretas a Objetos Inseguros, são uma das falhas mais comuns e perigosas em aplicações multitenant. Elas ocorrem quando uma consulta esquece de filtrar por locatário, permitindo que uma conta acesse os dados de outra conta.

Durante muito tempo, os IDORs eram difíceis de detectar. Eles não apareciam nas verificações de código e exigiam muito esforço manual. Por causa disso, muitos bugs IDOR só eram descobertos durante testes de penetração caros e trabalhosos, ou quando pesquisadores de segurança os encontravam por meio de programas de recompensa por bugs.

Mas isso mudou. As ferramentas de teste de segurança agênicas agora podem se comportar como utilizadores reais, clicando em fluxos de trabalho, alternando funções 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, também são mais fáceis de explorar. É por isso que as organizações não devem se concentrar apenas em detectar IDORs, mas também em preveni-las.

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

No que diz respeito à deteção, o AI Pentest Aikido já consegue encontrar vulnerabilidades IDOR, algo que SAST tradicionais baseadas em padrões nunca conseguiram fazer de forma fiável, porque o IDOR requer a compreensão do contexto de autorização, e não apenas padrões de código. O AI Pentest autentica-se como utilizadores reais, executa fluxos de trabalho completos e reutiliza identificadores de objetos em todas as funções para encontrar falhas IDOR realmente exploráveis. É por esta razão que muitas organizações que utilizam a nossa capacidade AI Pentest estão predominantemente interessadas em encontrar IDORs.

Mas encontrar vulnerabilidades IDOR é apenas metade da equação. Num mundo ideal, você impediria que elas fossem introduzidas em primeiro lugar. É disso que trata este post: proteção contra IDOR no Zen, firewall incorporado no aplicativo nosso firewall incorporado no aplicativo de código aberto firewall incorporado no aplicativo. Ele analisa todas as consultas SQL em tempo de execução e gera um erro se uma consulta estiver sem um filtro de locatário ou usar o ID de locatário errado, detectando o bug durante o desenvolvimento e os testes antes que ele chegue à produção.

Em muitos ambientes empresariais, especialmente durante revisões de segurança ou avaliações de fornecedores, uma questão recorrente é como a multilocação é aplicada e como o acesso aos dados entre locatários é impedido.

As equipas de segurança e a liderança querem garantias técnicas claras de que os limites dos inquilinos são aplicados sistematicamente, e não apenas por convenção.

Ter um mecanismo que valida automaticamente o escopo do locatário no nível da consulta fornece uma resposta direta e confiável. Isso muda a conversa de "dependemos dos desenvolvedores para lembrar disso" para "o sistema impõe isso automaticamente".

A configuração é a seguinte:

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 a sua aplicação tem contas, organizações, espaços de trabalho ou equipas, provavelmente tem uma coluna como identificação_do_inquilino que mantém os dados de cada conta separados. Mas quando a consulta esquece de filtrar essa coluna ou filtra o valor errado, isso significa que uma conta pode aceder aos dados de outra. Essa é uma vulnerabilidade IDOR.  

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

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á identificação_do_inquilino filtro. Se Alice enviar OBTER /encomendas/42 e o pedido 42 pertence a Bob, Alice recebe o pedido de Bob. Isso é um IDOR.

A correção é simples, basta adicionar um ONDE tenant_id = $2 cláusula. No entanto, o bug é fácil de introduzir e difícil de detectar, especialmente numa base de código grande com centenas de consultas em dezenas de ficheiros. Basta um filtro esquecido.

IDOR é uma categoria ampla. Também inclui coisas como aceder aos ficheiros de outros utilizadores através da manipulação de URL ou pontos finais de API que não verificam a propriedade. Esta publicação centra-se especificamente no subconjunto de filtragem de inquilinos SQL, garantindo que todas as consultas à base de dados sejam adequadamente direcionadas para o inquilino atual. Para uma análise mais aprofundada sobre IDOR de forma mais ampla, consulte a nossa publicação sobre vulnerabilidades IDOR explicadas.
O que existe hoje em dia

Além da verificação de segurança, que se concentra mais em encontrar bugs existentes, existem outros métodos para impedir a introdução de novos IDORs: bibliotecas no nível da estrutura e aplicação no nível do banco de dados. Cada um tem seus pontos fortes e limitações.

Bibliotecas ao nível da estrutura

Várias estruturas têm bibliotecas que limitam automaticamente as consultas ao inquilino atual:

  • Ruby on Rails: acts_as_tenant adiciona escopo automático de locatário aos modelos ActiveRecord. Declare acts_as_tenant(:account) e todas as consultas nesse modelo serão filtradas pelo locatário atual.
  • Django: o django-multitenant faz o mesmo para o ORM do Django. Defina o inquilino atual no middleware e Product.objects.all() automaticamente se torna um escopo do inquilino.
  • Laravel: O Tenancy para Laravel oferece multitenancy com banco de dados único e múltiplos bancos de dados, com troca automática de contexto.
  • .NET / EF Core: Filtros de consulta globais permite que você se inscreva ONDE tenant_id = X para todas as consultas automaticamente ao nível do modelo.

Essas bibliotecas funcionam bem dentro do seu próprio ORM. A limitação é que elas protegem apenas consultas que passam pela abstração do ORM. Consultas SQL brutas, consultas de outras bibliotecas ou consultas criadas com um construtor de consultas diferente no mesmo projeto não serão abrangidas. Elas também são opcionais, é preciso lembrar de adicionar a anotação a todos os modelos, e novos modelos podem passar despercebidos sem que ninguém perceba. Para ser justo, atua_como_inquilino tem um requer_inquilino configuração que gera um erro quando nenhum locatário é definido, o que reduz significativamente o risco de «esquecer de definir o locatário».

Existem também footguns subtis. No Rails, por exemplo, atua_como_inquilino funciona adicionando um escopo_padrão. Se um programador chamar Projeto.sem escopo para remover um escopo padrão diferente, como um arquivado filtro, ele remove todos os escopos padrão, incluindo o filtro de locatário, sem erros ou avisos. O Rails tem desfocar (sem o d) para remover cirurgicamente um único escopo, mas isso requer saber que o escopo do inquilino existe em primeiro lugar. Em uma base de código com muitos desenvolvedores, alguém acabará por recorrer ao sem escopo, e o limite do inquilino desaparece silenciosamente.

Aplicação ao nível da base de dados

Segurança ao nível da linha (RLS) do PostgreSQL vai um passo além, impondo o isolamento dos locatários no nível do banco de dados. Em vez de depender do seu aplicativo para adicionar ONDE tenant_id = ? para cada consulta, você diz ao próprio Postgres para aplicá-la:

-- 1. Ativar 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 POLÍTICA tenant_isolation ON projetos
  PARA TODOS
  USANDO (tenant_id = configuração_atual('app.current_tenant_id')::uuid);

-- 3. Por solicitação, defina o contexto do locatário antes de executar consultas
SET app.current_tenant_id = 'aaaa-aaaa-aaaa';

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

RLS é a garantia mais forte das abordagens listadas aqui. Consultas SQL brutas ou ORM, não importa. O banco de dados impõe isso. E, ao contrário de atua_como_inquilino, se você esquecer de definir a variável de sessão, nenhum dado será retornado, em vez de todos os dados. Essa é uma configuração padrão muito mais segura.

Mas isso acarreta compromissos 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 torna a depuração mais difícil. Um ATUALIZAÇÃO que deveria modificar 100 linhas pode afetar silenciosamente 0 devido a uma incompatibilidade de políticas, e é difícil distinguir isso de «não existem dados».

O agrupamento de ligações também aumenta a complexidade. RLS com SET não funciona corretamente com o pgBouncer no modo de pool de instruções ou transações, o que pode levar ao retorno de linhas para o locatário errado. Isso pode ocorrer apenas na produção.

Existem também limitações estruturais. Os superutilizadores ignoram todas as políticas completamente e as visualizações ignoram o RLS por predefinição, pelo que a sua aplicação deve ligar-se como uma função não superutilizadora. Por fim, é apenas para Postgres. Se precisar de suportar MySQL, SQLite para desenvolvimento ou outro armazenamento de dados, a sua camada de segurança não o acompanha.

A conclusão pragmática: o RLS é excelente como rede de segurança para o isolamento de locatários, mas a complexidade operacional e a dificuldade de depuração significam que não é uma solução pronta para todas as equipas.

Onde o Zen se encaixa

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

O Zen tem as suas próprias vantagens e desvantagens, para ser sincero. Como atua_como_inquilino, é necessário ligar para definirTenantId em cada solicitação. Se você esquecer, o Zen gera um erro, portanto, ele falha de forma evidente, em vez de silenciosa, mas é a mesma classe de configuração por solicitação. E, ao contrário do RLS, o Zen cobre apenas consultas executadas dentro da sua aplicação. Se alguém se conectar ao banco de dados diretamente, por exemplo, através do psql ou de um serviço separado sem o Zen, essas consultas não serão verificadas.

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

Como o Zen protege contra IDORs

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

Um analisador SQL adequado, escrito em Rust

No centro da proteção IDOR da Zen está um analisador SQL real construído no 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, em seguida, percorre a árvore para extrair:

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

Por que não usar expressões regulares? As expressões regulares funcionam bem para consultas simples, como SELECT * FROM encomendas WHERE tenant_id = ?. Mas as aplicações do mundo real têm CTEs, UNIONs, subconsultas, JOINs com aliases e todos os tipos de SQL válidos 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 é necessariamente errada, mas é difícil de manter e fácil de surpreender.

Um analisador adequado lida com tudo isso de forma imediata. Ele também reconhece corretamente instruções que não precisam ser verificadas, como instruções DDL (CREATE TABLE, ALTER TABLE), controlo de transações (BEGIN, COMMIT, ROLLBACK) e comandos de sessão (DEFINIR, MOSTRAR).

Aqui está como a análise se apresenta nos bastidores. Dada esta consulta:

SELECCIONAR * FROM encomendas
LEFT JOIN itens_da_encomenda ON pedidos.id = itens_da_encomenda.id_da_encomenda
ON pedidos.tenant_id = $1
E pedidos.estado = 'ativo';

O analisador 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 verifica então se todas as tabelas na consulta têm um filtro ativado. identificação_do_inquilinoe se o valor do filtro corresponde ao inquilino atual.

O mesmo se aplica a INSERIR, ATUALIZAÇÃO, e ELIMINARO Zen garante que a coluna do inquilino esteja sempre presente e tenha sempre o valor correto. Esses erros são lançados como erros, não apenas registados. O IDOR é um bug do desenvolvedor, não um ataque externo, então é melhor que ele apareça claramente durante o desenvolvimento e os testes, em vez de passar despercebido para a produção.

Desempenho

Analisar SQL em cada consulta parece dispendioso, mas, na prática, é rápido. A ideia principal é que a maioria das aplicações usa instruções preparadas ou consultas parametrizadas. A string SQL permanece a mesma, apenas os valores dos parâmetros mudam. Portanto, SELECT * FROM encomendas WHERE identificação_do_inquilino = $1 AND estado = $2 é analisada uma vez, e todas as execuções subsequentes dessa mesma consulta são um acerto de cache.

A primeira vez que o Zen vê uma nova string de consulta, o analisador Rust constrói a AST e extrai as tabelas e os filtros. Depois disso, basta uma pesquisa no cache e uma comparação do ID do locatário com o valor do espaço reservado resolvido.

Se estiver a incorporar valores diretamente em cadeias SQL, por exemplo, concatenação de cadeias em vez de consultas parametrizadas, cada cadeia única requer uma nova análise. Mas provavelmente não deve fazer isso de qualquer maneira. As consultas parametrizadas protegem contra injeção SQL e também tornam a verificação IDOR mais rápida.

O caminho para a produção: consumindo o nosso próprio produto

Implementámos a proteção IDOR da Zen em vários serviços internos Aikido. Isto revelou imediatamente casos extremos que precisavam de ser tratados.

O suporte a transações foi um obstáculo no início. Aplicações reais utilizam COMEÇAR, COMPROMETER-SE, e ROLLBACK, e o Zen precisava reconhecer essas declarações como seguras, que não exigem filtragem do locatário, em vez de considerá-las como erros. Adicionamos isso rapidamente depois de ver que falhou na nossa primeira implementação interna.

As expressões de tabela comuns (CTEs) foram outro desafio. Uma CTE como COM AS ativo (SELECT * FROM encomendas WHERE tenant_id = $1) cria uma tabela virtual que as consultas a jusante referenciam. O Zen precisava rastrear os nomes das CTE e excluí-los da lista de "tabelas reais", enquanto ainda analisava o corpo da CTE para uma filtragem adequada.

O sem identificação ou proteção escape também se revelou essencial. Nem todas as consultas precisam de filtragem de locatários, como painéis de administração, tarefas em segundo plano ou análises entre locatários. Inicialmente, tentámos um ignorarPróximaConsulta abordagem, em que você chamaria uma função antes da consulta para ignorar a verificação da próxima instrução SQL:

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

Na prática, isso revelou-se frágil. Com os pools de conexões, a "próxima consulta" em uma determinada conexão pode não ser aquela que você pretendia ignorar. O baseado em callback sem identificação ou proteção é explícito quanto ao âmbito. A proteção IDOR é desativada durante o período de retorno de chamada e nada mais.

Como protegemos a nossa API que fornece dados de ativos na nuvem

Um dos serviços que protegemos desde o início foi a API interna que fornece dados de ativos na nuvem em toda a plataforma.

Esta API é utilizada pela interface do utilizador, tarefas em segundo plano e vários mecanismos de segurança sempre que precisam de ler informações sobre a infraestrutura de um cliente. Ela está no centro do sistema e processa milhares de solicitações por segundo.

Como a plataforma é totalmente multitenant, o isolamento rigoroso dos tenants é fundamental. Cada consulta deve ser direcionada à organização correta, e não podemos confiar que os programadores se lembrarão de adicionar o filtro certo em cada caminho de código.

Antes do Zen oferecer suporte nativo à proteção IDOR, tínhamos uma implementação personalizada que impunha o escopo do locatário no nível da consulta. Quando o Zen introduziu suporte de primeira classe para esse comportamento, migramos da solução interna para a funcionalidade integrada, o que reduziu consideravelmente a quantidade de código que precisamos manter.

Hoje, o Zen verifica automaticamente se as consultas estão corretamente direcionadas ao locatário atual, mesmo sob carga pesada. Após introduzir a proteção IDOR do Zen, não observamos nenhum impacto perceptível no desempenho.

Detecção e prevenção

O AI Pentest Aikido encontra vulnerabilidades IDOR na sua aplicação em execução, simulando ataques reais. A proteção IDOR da Zen impede que elas sejam introduzidas, detectando filtros de locatários ausentes durante o desenvolvimento.

Juntos, eles cobrem os dois lados. O AI Pentest valida que o seu código existente é seguro, e o Zen garante que o novo código permaneça seguro. Use o AI Pentest para auditar o que já está implementado. Use o Zen para detectar erros à medida que os escreve.

Começando

A proteção IDOR está disponível hoje em @aikidosec/firewall para Node.js. Consulte 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.

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.