Apontei um revisor de IA para um serviço de pedidos de 2019: 63 comentários, nenhum era o IDOR
Um texto chamado "There is an AI code review bubble" juntou 351 pontos e 249 comentários no Hacker News. Fui ler a discussão esperando briga de ferramenta e achei outra coisa: muita gente relatando o mesmo tipo de achado. Nomeação, early return, sugestão de tipagem. Quase ninguém contando que o agente pegou uma falha de autorização.
Eu tinha um repositório na mão pra testar isso.
O alvo
Serviço de pedidos rodando em produção desde 2019. Node, Express, Postgres, perto de 64 mil linhas, três devs no histórico e nenhum deles ainda na empresa. Apontei o revisor para um PR de manutenção que encostava em 22 arquivos. Ele devolveu 63 comentários em uns 4 minutos.
Classifiquei os 63 na mão, um por um:
- nomeação de variável e estilo: 28
- early return e complexidade ciclomática: 11
- tipagem e checagem de null: 9
- try/catch engolindo erro sem log: 7
- consulta ao banco dentro de laço: 5
- etiquetados como segurança: 3
Os três de segurança eram validação de entrada e um log que imprimia o corpo inteiro da requisição. Achados legítimos. Nenhum deles falava sobre quem pode ler o quê.
O que ele passou reto
Enquanto o agente rodava, abri o serviço no navegador com o meu usuário, troquei um dígito na URL e li o pedido de outro cliente. Uns vinte minutos, sem ferramenta nenhuma. O id era sequencial e a tela escondia o botão, mas o endpoint não escondia nada.
O handler é este, quase sem edição:
router.get('/orders/:id', auth, async (req, res) => {
const { rows } = await db.query(
'SELECT * FROM orders WHERE id = $1',
[req.params.id]
);
if (!rows.length) {
return res.status(404).json({ error: 'order not found' });
}
return res.json(rows[0]);
});
Reproduzindo com dois tokens de clientes diferentes:
ID=$(curl -s -H "Authorization: Bearer $TOKEN_A" \
localhost:3000/api/orders | jq -r '.[0].id')
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $TOKEN_B" \
localhost:3000/api/orders/$ID
# 200
Esse 200 devia ser 404. O cliente B leu o pedido do cliente A, com valor, endereço de entrega e itens.
Por que o agente não vê isso
Olha o handler de novo e tenta achar o defeito olhando só pra ele. A consulta está parametrizada, então não tem injection. O 404 está no lugar. O auth está aplicado na rota. Nada ali está escrito errado.
O problema é uma cláusula que nunca existiu. O middleware auth responde quem é você, e não se aquele registro é seu. A regra de propriedade mora fora do arquivo: parte no schema, parte na cabeça de quem escreveu a primeira versão em 2019. Revisor de diff enxerga mal o que está ausente, e enxerga pior ainda quando a informação que provaria a ausência está em outro diretório.
Tem uma discussão recorrente aqui no TabNews sobre o quanto o agente realmente segue o que você escreve no AGENTS.md. Meu resultado abaixo dá um pedaço da resposta.
Como rodar esse teste no seu repositório
Três passos, uns quarenta minutos num projeto de porte médio.
- Liste as rotas que recebem identificador na URL.
rg -n "router\.(get|put|patch|delete)\(['\"][^'\"]*:\w*[Ii]d" src/ \
| tee /tmp/rotas.txt | wc -l
# 41
- Separe as que nunca mencionam dono em lugar nenhum do arquivo.
for f in $(rg -l ":id" src/routes); do
rg -q "req\.user|userId|customer_id|tenant_id|account_id" "$f" \
|| echo "$f"
done
Saída no meu caso:
src/routes/orders.js
src/routes/invoices.js
src/routes/attachments.js
src/routes/addresses.js
Quatro arquivos, 12 rotas das 41. Isso não é laudo de vulnerabilidade, é uma lista de suspeitos. Falso positivo aparece quando o filtro está numa camada de repositório, e falso negativo aparece quando o arquivo cita req.user numa rota e esquece na outra.
- Confirme cada suspeito com os dois tokens e o
curlde cima. Aqui não tem heurística, tem resposta HTTP. Das 12, sete devolveram 200 pro cliente errado. Uma delas era o anexo do pedido, que serve PDF direto do storage.
A correção é chata e é isso mesmo:
const { rows } = await db.query(
'SELECT * FROM orders WHERE id = $1 AND customer_id = $2',
[req.params.id, req.user.customerId]
);
Reapontei o agente, agora com a regra escrita
Botei no arquivo de instrução, em uma linha, que toda leitura de recurso de cliente precisa filtrar por customer_id vindo do token, e que rota com :id sem esse filtro deve ser reprovada.
Das sete rotas quebradas, ele marcou quatro. Melhorou bastante, e mesmo assim eu conferi as sete na mão, porque a que ele deixou passar era justamente a do anexo, onde a query estava dentro de um service em outro arquivo.
O que ficou pra mim depois do teste: o revisor de IA é ótimo em cobrir a superfície do texto e não substitui a pergunta que só o humano faz, que é "esse dado é dessa pessoa?". Cobertura de comentário virou a nova cobertura de teste. Sessenta e três comentários num PR dão a sensação de repositório vigiado do mesmo jeito que 92% de cobertura dão sensação de suíte confiável.
Uma coisa que eu não sei responder: se a instrução tivesse chegado como um teste executável em vez de texto no AGENTS.md, ele pegaria as sete? Não testei.
Quem aí já rodou revisor de IA em legado com autorização espalhada? Ele achou algum IDOR sem você ter dito onde procurar, ou também ficou nas 28 sugestões de nomeação?
Fonte: escrevi uma versão mais longa desse caso, com a parte de onde a revisão automática substitui gente e onde ela não substitui, em https://revin.com.br/pt/blog/code-review-automatizado-por-ia-onde-substitui-humano-e-onde-nao-substitui