Segurança em APIs Node.js: OWASP Top 10 na Prática
A maioria das APIs Node.js em produção falha em pelo menos 4 dos 10 itens do OWASP
O OWASP Top 10 para APIs (edição 2023) não é uma lista teórica. É um catálogo de vulnerabilidades que aparecem em auditorias reais, em APIs reais, escritas por times competentes. O problema é que a maioria dos devs lê o documento uma vez, concorda com tudo e não implementa nada.
Este post pega os itens mais relevantes para APIs Node.js (Express e Fastify), mostra o código vulnerável, explica por que quebra e entrega a correção funcional. Sem abstrações vagas: cada bloco de código roda.
Se você já tem uma API em produção e quer saber onde estão os buracos, comece pela matriz de priorização no final. Se quer entender cada vulnerabilidade, leia na ordem.
API1:2023 — Broken Object Level Authorization (BOLA)
A vulnerabilidade mais comum em APIs REST. O endpoint recebe um ID, busca o recurso e retorna sem verificar se o usuário autenticado tem permissão para acessá-lo.
// VULNERÁVEL: qualquer usuário autenticado acessa qualquer pedido
import express from "express";
import { prisma } from "./db";
const app = express();
app.get("/orders/:id", async (req, res) => {
const order = await prisma.order.findUnique({
where: { id: req.params.id },
});
if (!order) return res.status(404).json({ error: "Pedido não encontrado" });
// Retorna sem checar se o pedido pertence ao usuário logado
return res.json(order);
});
A correção é filtrar no nível da query, não depois:
// CORRIGIDO: a query já inclui o userId como filtro
import express from "express";
import { prisma } from "./db";
import { authenticate } from "./middleware/auth";
const app = express();
app.get("/orders/:id", authenticate, async (req, res) => {
const order = await prisma.order.findFirst({
where: {
id: req.params.id,
// Filtrar na query garante que o banco nunca retorna dado alheio,
// mesmo que a lógica de aplicação tenha bugs depois
userId: req.user.id,
},
});
if (!order) return res.status(404).json({ error: "Pedido não encontrado" });
return res.json(order);
});
A diferença entre findUnique + checagem posterior e findFirst com filtro composto parece sutil, mas importa: se você checa depois da query, qualquer refatoração que esqueça o if vaza dados. Quando o filtro está na query, o banco é a barreira. Esse padrão se conecta diretamente com a modelagem de domínio que discutimos em Domain-Driven Design na Prática com TypeScript e Prisma: o aggregate root controla o acesso.
API2:2023 — Broken Authentication
Tokens JWT sem validação adequada, sem expiração curta, sem rotação. O erro clássico é confiar no payload do token sem verificar a assinatura com o algoritmo correto.
// VULNERÁVEL: aceita qualquer algoritmo que o token declare
import jwt from "jsonwebtoken";
function verifyToken(token: string) {
// algorithms não especificado = aceita "none" em algumas versões
return jwt.verify(token, process.env.JWT_SECRET!);
}
// CORRIGIDO: força algoritmo, valida issuer e audience
import jwt from "jsonwebtoken";
interface TokenPayload {
sub: string;
role: string;
}
function verifyToken(token: string): TokenPayload {
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
// Forçar HS256 impede ataques de troca de algoritmo (ex: "none" ou RS256 com chave pública)
algorithms: ["HS256"],
issuer: "vivo-de-codigo-api",
audience: "vivo-de-codigo-client",
maxAge: "15m",
}) as TokenPayload;
return payload;
}
O maxAge: "15m" não substitui refresh tokens. Para APIs com sessões longas, combine access token curto (15 min) com refresh token opaco armazenado no banco, revogável. O post sobre APIs reais que não quebram em produção cobre a arquitetura de camadas onde essa validação se encaixa.
API3:2023 — Broken Object Property Level
Leia o artigo completo em https://www.vivodecodigo.com.br/backend/seguranca-api-nodejs-owasp-top-10-pratica