1

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

Carregando publicação patrocinada...