A01 Broken Access Control
A autenticação diz quem você é; o controle de acesso diz o que você pode fazer. Quando o servidor esquece de verificar em cada requisição, trocar um id na URL é tudo o que um atacante precisa.
O que é
O controle de acesso aplica a política para que usuários não ajam fora das permissões previstas. Ele quebra quando o servidor confia em algo que o cliente controla: um id na URL, um campo oculto, uma claim de papel, um cookie. O resultado é ler dados de outras pessoas, alterá-los ou alcançar funções de administração.
O :2025 o lista como A01. As duas formas do dia a dia são (insecure direct object reference, também chamado em segurança de APIs): a requisição nomeia um objeto e o servidor o devolve sem perguntar se quem chama é o dono; e a falta de verificações no nível de função: existe um endpoint para administradores, mas nada impede um usuário comum de chamá-lo.
Como o ataque funciona
O atacante faz login como usuário comum, abre a aba de rede do navegador e encontra uma requisição como GET /api/invoices/1041. Troca 1041 por 1042 e lê a fatura de outra pessoa. Se os ids forem sequenciais, um script de dez linhas percorre a tabela inteira.
- Altere um id no path, na query string, no corpo JSON ou em um header, e veja se a resposta muda de dono.
- Chame diretamente uma rota de admin ou interna (/admin/users, DELETE /api/users/7) com uma sessão comum.
- Envie campos extras que o formulário nunca mostra ("role": "admin"): o deixa o cliente definir o que não deveria.
- Esconder o botão na UI: a UI não é um controle, o servidor é.
Código vulnerável
Cada handler verifica se o usuário está logado e depois carrega o objeto apenas pelo id. Nada liga a fatura a quem chama.
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await db.invoice.findUnique({ where: { id: Number(req.params.id) } });
res.json(invoice); // qualquer usuário logado pode ler qualquer fatura
});@app.get("/api/invoices/<int:invoice_id>")
@login_required
def get_invoice(invoice_id):
invoice = Invoice.query.get_or_404(invoice_id)
return jsonify(invoice.to_dict())@GetMapping("/api/invoices/{id}")
public Invoice get(@PathVariable long id) {
return repo.findById(id).orElseThrow(NotFoundException::new);
}A correção
Coloque a propriedade na própria consulta, de modo que uma linha que não é sua simplesmente não seja encontrada. Responda da mesma forma para "não existe" e "não é sua", para que a resposta não confirme quais ids são válidos. A regra é a mesma em qualquer linguagem: autorize no servidor, em cada requisição, negando por padrão.
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
const invoice = await db.invoice.findFirst({
where: { id: Number(req.params.id), ownerId: req.user.id },
});
if (!invoice) return res.status(404).end();
res.json(invoice);
});@app.get("/api/invoices/<int:invoice_id>")
@login_required
def get_invoice(invoice_id):
invoice = Invoice.query.filter_by(
id=invoice_id, owner_id=current_user.id
).first_or_404()
return jsonify(invoice.to_dict())@GetMapping("/api/invoices/{id}")
public Invoice get(@PathVariable long id, @AuthenticationPrincipal User user) {
return repo.findByIdAndOwnerId(id, user.getId())
.orElseThrow(NotFoundException::new);
}const requireRole = (role) => (req, res, next) =>
req.user?.roles?.includes(role) ? next() : res.status(403).end();
app.delete("/api/users/:id", requireLogin, requireRole("admin"), deleteUser);Prove a correção
Um teste de regressão usa dois usuários. O usuário A cria uma fatura; o usuário B a solicita e deve receber 404 (ou 403), e uma chamada anônima deve receber 401. Rode-o no CI para que um refactor não remova a verificação sem ninguém notar. O mesmo formato de teste funciona em qualquer linguagem e para qualquer tipo de objeto.
def test_user_cannot_read_other_users_invoice(client, alice, bob):
invoice_id = client.post("/api/invoices", json={"total": 10}, auth=alice).json()["id"]
assert client.get(f"/api/invoices/{invoice_id}", auth=alice).status_code == 200
assert client.get(f"/api/invoices/{invoice_id}", auth=bob).status_code == 404
assert client.get(f"/api/invoices/{invoice_id}").status_code == 401test("bob cannot read alice's invoice", async () => {
const { body } = await request(app).post("/api/invoices").set(alice).send({ total: 10 });
await request(app).get(`/api/invoices/${body.id}`).set(bob).expect(404);
await request(app).get(`/api/invoices/${body.id}`).expect(401);
});Checklist
- : uma rota sem regra explícita é proibida, não aberta.
- Aplique a propriedade em um único lugar compartilhado (um helper de consulta, um policy engine), e não copiando verificações em cada handler.
- Não confie em ids, papéis ou preços que vêm do cliente. Derive a identidade da sessão.
- Use ids imprevisíveis quando útil (UUIDs), mas nunca como único controle: um id impossível de adivinhar não é autorização.
- Registre as falhas de controle de acesso e alerte sobre rajadas: alguém enumerando ids é visível.
- Teste com duas contas a cada release. Scanners automatizados raramente entendem quem deveria ver o quê.
conexões no mapa
- A06 Insecure Designdefine as regras deA01 Broken Access Control
- A01 Broken Access Controlprecisa de uma identidade confiável deA07 Authentication Failures
- A05 Injectionpode contornarA01 Broken Access Control
- A09 Security Logging and Alerting Failuresdetecta o abuso deA01 Broken Access Control
atualizado em 14/10/2026 · fontes para aprofundar · reportar erro
em outros domínios
- Testes de segurança de web e APIoffseccomo encontrar o que o outro ensina a corrigir
- IAM - identidade e acessocloudseca mesma regra de menor privilégio, na nuvem
Desbloqueia
teste o que entendeu
Carregando perguntas…