Em 2026, as APIs são o alvo preferido dos atacantes. Segundo o relatório IBM Cost of a Data Breach 2024, o custo médio de uma violação de dados atingiu $4,88 milhões, com ataques a APIs a representar uma fatia crescente desse valor. O relatório Salt Security State of API Security 2025 indica que mais de 94% das organizações sofreram pelo menos um incidente de segurança em APIs nos últimos 12 meses.

Para um pentester ou programador que queira auditar as suas próprias APIs Node.js, o processo pode parecer intimidante: dezenas de ferramentas, centenas de técnicas e um inventário crescente de vulnerabilidades. Este tutorial apresenta um processo de 12 passos para testar a segurança de APIs Node.js de forma metódica, com código funcional, exemplos de output e os erros mais comuns a evitar. Todo o processo pode ser concluído em menos de 4 horas para uma API de tamanho médio, usando ferramentas gratuitas ou de custo reduzido.

Por que o Pentest de APIs É Crítico em 2026

As APIs REST e GraphQL tornaram-se a espinha dorsal das aplicações modernas. Uma aplicação Node.js típica expõe dezenas de endpoints, cada um com lógica de autorização própria, validação de input e acesso a dados sensíveis. Um único endpoint mal configurado pode comprometer toda a base de dados.

O problema não é a falta de boas práticas, mas sim a velocidade de desenvolvimento. Equipas que publicam código várias vezes por dia têm dificuldade em manter revisões de segurança manuais. O OWASP API Security Top 10 de 2023 documenta as vulnerabilidades mais críticas, mas muitos programadores nunca as testam em produção antes de um incidente real.

A realidade é que um atacante profissional, ou um pentester experiente, leva em média menos de 20 minutos a encontrar a primeira vulnerabilidade crítica numa API Node.js sem proteção adequada. Testar antes que o atacante o faça é a única forma de garantir que o código resiste. O investimento em testes de penetração regulares reduz o custo médio de uma violação em mais de $1,7 milhões, segundo o IBM Security.

Categoria OWASP API Top 10 2023CódigoFrequênciaImpacto Típico
Broken Object Level Authorization (BOLA)API1:2023Muito altaAcesso a dados de terceiros
Broken AuthenticationAPI2:2023AltaTomada de conta
Broken Object Property Level AuthorizationAPI3:2023AltaModificação não autorizada
Unrestricted Resource ConsumptionAPI4:2023MédiaIndisponibilidade, custo elevado
Broken Function Level Authorization (BFLA)API5:2023MédiaAcesso a funções administrativas
Unrestricted Access to Sensitive Business FlowsAPI6:2023MédiaFraude, abuso de negócio
Server Side Request Forgery (SSRF)API7:2023CrescenteAcesso a redes internas
Security MisconfigurationAPI8:2023AltaMúltiplos vetores de ataque
Improper Inventory ManagementAPI9:2023AltaAPIs esquecidas, sem patch
Unsafe Consumption of APIsAPI10:2023Baixa/MédiaConfiança excessiva em terceiros

Pré-requisitos e Ferramentas Necessárias

Antes de começar, certifique-se de que tem autorização explícita e por escrito para testar os sistemas alvo. Testes de penetração sem autorização são ilegais em Portugal ao abrigo do Decreto-Lei 109-D/2021 (Lei do Cibercrime). Todos os testes neste tutorial devem ser realizados em ambientes de desenvolvimento ou staging próprios, nunca em sistemas de produção de terceiros.

FerramentaVersãoCustoFinalidade
Node.js22.x LTSGrátisRuntime e scripts de teste
OWASP ZAP2.16.xGrátisScan automatizado de vulnerabilidades
Burp Suite Community2025.xGrátisProxy de intercepção e análise manual
SQLmapÚltima estávelGrátisDeteção de injeção SQL
Nmap7.95+GrátisEnumeração de portas e serviços
supertest (npm)7.xGrátisTestes automatizados de endpoints HTTP
jest (npm)29.xGrátisFramework de testes e asserções
artillery (npm)2.xGrátisTestes de carga e rate limiting

Instale as dependências de teste no projeto Node.js:

npm install --save-dev supertest jest
npm install -g artillery

Passo 1: Criar a API de Laboratório Node.js

Para praticar com segurança, comece por criar uma API Node.js vulnerável num ambiente isolado. Esta API serve de alvo para todos os testes seguintes. Nunca execute testes em APIs de produção sem autorização e janela de manutenção acordada.

// api-vulneravel.js - APENAS PARA TESTES EM AMBIENTE DE LAB
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

const utilizadores = [
  { id: 1, email: '[email protected]', senha: 'senha123', admin: false },
  { id: 2, email: '[email protected]', senha: 'abc456', admin: false },
  { id: 3, email: '[email protected]', senha: 'admin999', admin: true },
];

const JWT_SECRET = 'segredo-fraco'; // Vulnerabilidade intencional

// VULNERÁVEL: Sem proteção de timing attack
app.post('/login', (req, res) => {
  const { email, senha } = req.body;
  const user = utilizadores.find(u => u.email === email && u.senha === senha);
  if (!user) return res.status(401).json({ erro: 'Credenciais inválidas' });
  const token = jwt.sign({ userId: user.id }, JWT_SECRET);
  res.json({ token });
});

// VULNERÁVEL: BOLA - sem verificação de propriedade do recurso
app.get('/utilizadores/:id', (req, res) => {
  const user = utilizadores.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ erro: 'Não encontrado' });
  res.json(user); // Expõe a senha e todos os campos!
});

// VULNERÁVEL: Sem limitação de taxa e sem sanitização
app.post('/pesquisar', (req, res) => {
  const { q } = req.body;
  const resultados = utilizadores.filter(u => u.email.includes(q));
  res.json(resultados);
});

app.listen(3000, () => console.log('API de lab na porta 3000'));

Inicie o servidor com node api-vulneravel.js e confirme que responde:

curl http://localhost:3000/utilizadores/1

# Output (expõe senha - vulnerabilidade BOLA):
# {"id":1,"email":"[email protected]","senha":"senha123","admin":false}

Passo 2: Reconhecimento e Mapeamento da Superfície de Ataque

O reconhecimento é sempre o primeiro passo. Mesmo conhecendo o código-fonte (teste de caixa branca), é essencial verificar o que a API expõe externamente e quais os endpoints acessíveis. O Nmap identifica portas abertas, serviços e versões de software.

Enumeração com Nmap

Consulte o guia Nmap: Auditar a Rede em 12 Passos para referência completa. Para APIs Node.js, o seguinte comando é suficiente para o reconhecimento inicial:

nmap -sV -sC -p 80,443,3000,8080,8443 localhost

# Output esperado:
# PORT     STATE SERVICE VERSION
# 3000/tcp open  http    Node.js Express framework
# |_http-title: Site doesn't have a title
# |_http-server-header: Express

A presença do cabeçalho X-Powered-By: Express confirma a tecnologia usada. Este cabeçalho deve ser removido em produção com app.disable('x-powered-by') ou com Helmet.js.

Descoberta de Endpoints com Gobuster

# Varredura de endpoints comuns de APIs REST
gobuster dir -u http://localhost:3000 \
  -w /usr/share/wordlists/dirb/common.txt \
  -x json \
  --status-codes 200,201,401,403

# Output:
# /login                (Status: 405) [Size: 31]
# /utilizadores         (Status: 200) [Size: 238]
# /pesquisar            (Status: 405) [Size: 31]

Passo 3: Análise Automática com OWASP ZAP

O OWASP ZAP (Zed Attack Proxy) é o scanner de segurança de código aberto mais usado por pentesters. A versão 2.16.x inclui uma API REST completa para integração em pipelines de CI/CD. Para comparar ZAP com Burp Suite em detalhe, consulte o artigo Burp Suite vs OWASP ZAP: $449/ano vs Grátis.

Para APIs Node.js, o fluxo mais eficaz é o ZAP em modo daemon com API REST:

# Iniciar ZAP em modo headless
zap.sh -daemon -port 8090 -config api.key=minhachave123

# Spider para descobrir endpoints
curl "http://localhost:8090/JSON/spider/action/scan/?apikey=minhachave123&url=http://localhost:3000"

# Iniciar scan ativo de vulnerabilidades
curl "http://localhost:8090/JSON/ascan/action/scan/?apikey=minhachave123&url=http://localhost:3000"

# Aguardar conclusão (polling)
until [ "$(curl -s 'http://localhost:8090/JSON/ascan/view/status/?apikey=minhachave123' | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"status\"])')" = "100" ]; do
  echo "Scan em curso..."
  sleep 5
done

# Obter alertas
curl -s "http://localhost:8090/JSON/core/view/alerts/?apikey=minhachave123" | \
  python3 -c "
import json,sys
data=json.load(sys.stdin)
for a in data.get('alerts',[]):
    print(f'{a[\"risk\"]:8} | {a[\"alert\"]}')
"

# Output esperado:
# High     | SQL Injection
# High     | Weak Authentication Method
# Medium   | X-Content-Type-Options Header Missing
# Medium   | Content Security Policy (CSP) Header Not Set
# Low      | X-Frame-Options Header Not Set

Passo 4: Testar Autenticação e Vulnerabilidades JWT

A autenticação quebrada (API2:2023) é a segunda categoria mais crítica do OWASP API Top 10. Para APIs Node.js com JWT, há três ataques principais a testar. Consulte o artigo JWT Authentication in Node.js para a implementação segura.

Ataque 1: Algoritmo none

Algumas implementações JWT aceitam tokens com o campo alg definido como none, ignorando completamente a assinatura:

# Criar token JWT com alg:none (sem assinatura)
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
PAYLOAD=$(echo -n '{"userId":3,"iat":1717000000}' | base64 -w0 | tr '+/' '-_' | tr -d '=')

TOKEN_NONE="${HEADER}.${PAYLOAD}."

# Testar contra a API
curl -H "Authorization: Bearer $TOKEN_NONE" http://localhost:3000/utilizadores/3

# Se vulnerável, retorna dados do admin:
# {"id":3,"email":"[email protected]","senha":"admin999","admin":true}

Ataque 2: Força Bruta ao Segredo JWT

# Instalar jwt_tool para análise e ataques JWT
pip3 install jwt_tool

# Capturar um token válido via login
TOKEN=$(curl -s -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","senha":"senha123"}' | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

# Tentar descobrir o segredo por dicionário
jwt_tool $TOKEN -C -d /usr/share/wordlists/rockyou.txt

# Se o segredo for fraco:
# [+] segredo-fraco is the CORRECT key!
# [*] Cracked in 0.23 seconds
# [*] Use flag -T to tamper with the token

Correção: Use segredos JWT com entropia mínima de 256 bits e prefira algoritmos assimétricos como RS256 ou ES256:

const crypto = require('crypto');

// Gerar segredo com 256 bits de entropia (para HS256)
const JWT_SECRET = crypto.randomBytes(32).toString('hex');

// Ou usar par de chaves assimétricas (recomendado para produção)
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
});

// Assinar com chave privada RS256
const token = jwt.sign({ userId: user.id }, privateKey, {
  algorithm: 'RS256',
  expiresIn: '1h',
});

// Verificar com chave pública - algoritmo específico obrigatório
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Passo 5: Detetar Injeção SQL com SQLmap

A injeção SQL continua a ser uma das vulnerabilidades mais exploradas, mesmo com ORMs modernos. O SQLmap automatiza a deteção e exploração de falhas de injeção em APIs REST que aceitam JSON.

# Testar endpoint POST com SQLmap
sqlmap -u "http://localhost:3000/pesquisar" \
  --data='{"q":"test"}' \
  --content-type="application/json" \
  --level=3 \
  --risk=2 \
  --batch

# Output se vulnerável:
# [INFO] POST parameter 'JSON q' appears to be injectable
# [INFO] AND boolean-based blind - WHERE or HAVING clause

# Extrair dados com SQLmap (prova de conceito)
sqlmap -u "http://localhost:3000/pesquisar" \
  --data='{"q":"test"}' \
  --content-type="application/json" \
  --dump \
  --batch

Para testes manuais, os payloads mais eficazes para APIs Node.js com SQLite, PostgreSQL ou MySQL:

# Boolean-based blind injection
curl -X POST http://localhost:3000/pesquisar \
  -H "Content-Type: application/json" \
  -d '{"q":"test'"'"' AND 1=1 --"}'

# Time-based blind (detecta quando não há output visível)
curl -X POST http://localhost:3000/pesquisar \
  -H "Content-Type: application/json" \
  -d '{"q":"'"'"' OR SLEEP(5) --"}'
# Se a resposta demorar 5+ segundos, há injeção time-based

# Union-based (extração direta de dados)
curl -X POST http://localhost:3000/pesquisar \
  -H "Content-Type: application/json" \
  -d '{"q":"'"'"' UNION SELECT email,senha,NULL FROM utilizadores --"}'

Correção: Use sempre queries parametrizadas com um ORM ou o driver nativo. Nunca construa queries SQL por concatenação:

// MAU: Vulnerável a SQL Injection
const query = `SELECT * FROM users WHERE email LIKE '%${q}%'`;

// BOM: Query parametrizada com pg (PostgreSQL)
const { rows } = await pool.query(
  'SELECT id, email FROM users WHERE email LIKE $1',
  [`%${q}%`]
);

// BOM: Com Prisma ORM (proteção automática)
const users = await prisma.user.findMany({
  where: { email: { contains: q } },
  select: { id: true, email: true }, // Nunca incluir senha
});

Passo 6: Testar Controlo de Acesso (BOLA e BFLA)

O Broken Object Level Authorization (BOLA, também conhecido como IDOR) é a vulnerabilidade número 1 do OWASP API Top 10 2023. Ocorre quando um utilizador acede a recursos de outros utilizadores apenas alterando um ID na URL. Um pentester testa isto sistematicamente em todos os endpoints com identificadores.

# Passo 1: Autenticar como utilizador 1 (alice)
TOKEN=$(curl -s -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","senha":"senha123"}' | \
  python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

echo "Token de Alice obtido"

# Passo 2: Testar BOLA - Alice aceder ao perfil de Bob (ID 2)
echo "=== Teste BOLA: Acesso ao recurso de Bob ==="
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/utilizadores/2
# Resposta vulnerável: {"id":2,"email":"[email protected]","senha":"abc456","admin":false}
# Resposta segura: {"erro":"Acesso negado"} (HTTP 403)

# Passo 3: Escalada de privilégio - Alice tentar aceder ao admin (ID 3)
echo "=== Teste BOLA: Escalada para admin ==="
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/utilizadores/3
# Resposta vulnerável: {"id":3,"email":"[email protected]","senha":"admin999","admin":true}

# Passo 4: BFLA - tentar funções administrativas como utilizador normal
echo "=== Teste BFLA: Funções admin ==="
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/admin/utilizadores/2
# Resposta correta: 403 Forbidden
# Resposta vulnerável: 200 OK

Correção BOLA: Implemente verificação de propriedade em cada endpoint que acede a recursos específicos:

// Middleware de autenticação
const autenticar = (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ erro: 'Token obrigatório' });
  try {
    req.utilizador = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
    next();
  } catch {
    res.status(401).json({ erro: 'Token inválido ou expirado' });
  }
};

// Endpoint com verificação BOLA correta
app.get('/utilizadores/:id', autenticar, async (req, res) => {
  const idSolicitado = parseInt(req.params.id);

  // O utilizador só pode aceder ao próprio perfil, exceto admins
  if (idSolicitado !== req.utilizador.userId && !req.utilizador.admin) {
    return res.status(403).json({ erro: 'Acesso negado' });
  }

  const user = await User.findById(idSolicitado);
  if (!user) return res.status(404).json({ erro: 'Não encontrado' });

  // NUNCA retornar a senha
  const { senha, ...dadosSeguros } = user;
  res.json(dadosSeguros);
});

Passo 7: Verificar Limitação de Taxa e Ataques de Força Bruta

Sem rate limiting, qualquer endpoint de autenticação fica vulnerável a ataques de força bruta. Um pentester testa esta proteção com ferramentas de carga. O artigo Rate Limiting em Node.js: 12 Passos cobre a implementação com express-rate-limit e Redis.

# Criar configuração artillery para teste de força bruta
cat > teste-rate-limit.yml << 'EOF'
config:
  target: "http://localhost:3000"
  phases:
    - duration: 10
      arrivalRate: 50
      name: "Simulação de força bruta"

scenarios:
  - name: "Tentativas de login com senhas aleatórias"
    flow:
      - post:
          url: "/login"
          json:
            email: "[email protected]"
            senha: "{{ $randomString() }}"
EOF

# Executar teste
artillery run teste-rate-limit.yml

# Output sem rate limiting (problema crítico):
# Requests completed:   500
# HTTP 401 (credenciais erradas):  500
# HTTP 429 (bloqueado):              0
# --> Todas as 500 tentativas passaram sem bloqueio!

Adicionalmente, teste o bypass de rate limiting via manipulação de cabeçalhos:

# Testar bypass via X-Forwarded-For
for i in $(seq 1 20); do
  STATUS=$(curl -s -X POST http://localhost:3000/login \
    -H "Content-Type: application/json" \
    -H "X-Forwarded-For: 10.0.0.$i" \
    -d '{"email":"[email protected]","senha":"errada"}' \
    -o /dev/null -w '%{http_code}')
  echo "IP fake 10.0.0.$i -> HTTP $STATUS"
done

# Se todos retornarem 401 (e não 429), o rate limiting
# confia no cabeçalho X-Forwarded-For sem validação - vulnerável!

Passo 8: Verificar Cabeçalhos de Segurança HTTP

Os cabeçalhos HTTP de segurança são uma camada de defesa que muitos programadores ignoram. Um pentester verifica sempre a presença e configuração correta destes cabeçalhos como parte do teste de API8:2023 – Security Misconfiguration.

# Verificar cabeçalhos de segurança com curl
curl -I http://localhost:3000/utilizadores/1

# Output de uma API sem proteção (problemático):
# HTTP/1.1 200 OK
# X-Powered-By: Express           <-- Expõe tecnologia
# Content-Type: application/json
# Access-Control-Allow-Origin: *  <-- CORS completamente aberto
# (sem Strict-Transport-Security)
# (sem X-Content-Type-Options)
# (sem X-Frame-Options)
Cabeçalho HTTPValor RecomendadoRisco se Ausente
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDowngrade HTTPS para HTTP, MITM
X-Content-Type-OptionsnosniffMIME sniffing, XSS via upload
X-Frame-OptionsDENYClickjacking
Content-Security-PolicyDiretivas específicas por aplicaçãoXSS, injeção de conteúdo
X-Powered-ByRemoverFingerprinting da tecnologia
Referrer-Policystrict-origin-when-cross-originFuga de informação sensível

Para automatizar esta verificação num script de teste:

#!/usr/bin/env node
// verificar-cabecalhos.js
const https = require('https');
const http = require('http');

const CABECALHOS_OBRIGATORIOS = [
  'strict-transport-security',
  'x-content-type-options',
  'x-frame-options',
  'content-security-policy',
];

async function verificarCabecalhos(url) {
  return new Promise((resolve) => {
    const client = url.startsWith('https') ? https : http;
    client.get(url, (res) => {
      const cabecalhos = res.headers;
      const resultados = CABECALHOS_OBRIGATORIOS.map(cab => ({
        cabecalho: cab,
        presente: !!cabecalhos[cab],
        valor: cabecalhos[cab] || 'AUSENTE',
      }));
      resolve(resultados);
    });
  });
}

(async () => {
  const resultados = await verificarCabecalhos('http://localhost:3000/');
  resultados.forEach(r => {
    const status = r.presente ? 'OK' : 'FALHA';
    console.log(`[${status}] ${r.cabecalho}: ${r.valor}`);
  });
})();

Passo 9: Testar CORS Mal Configurado

Uma configuração CORS incorreta permite que sites maliciosos façam pedidos autenticados à API em nome dos utilizadores. É uma das vulnerabilidades mais fáceis de explorar e mais difíceis de detetar sem testes específicos.

# Teste 1: Reflexão de Origin (vulnerabilidade crítica)
curl -I http://localhost:3000/utilizadores/1 \
  -H "Origin: https://atacante.exemplo.com"

# Se a resposta incluir ambos:
# Access-Control-Allow-Origin: https://atacante.exemplo.com
# Access-Control-Allow-Credentials: true
# -> A API aceita qualquer origem com credenciais. Crítico!

# Teste 2: Origem nula (bypass comum)
curl -I http://localhost:3000/utilizadores/1 \
  -H "Origin: null"

# Teste 3: Verificar se aceita subdomínios sem validação
curl -I http://localhost:3000/utilizadores/1 \
  -H "Origin: https://atacante.meudominio.pt"

# Exploração: HTML que um atacante pode usar para roubar dados
# (executado no browser da vítima após visitar site malicioso)
# 

Correção: Configure CORS com lista branca explícita de origens:

const cors = require('cors');

const origensPermitidas = [
  'https://app.meudominio.pt',
  'https://admin.meudominio.pt',
];

app.use(cors({
  origin: (origin, callback) => {
    // Permitir pedidos sem origem (ex: mobile apps, curl)
    if (!origin) return callback(null, true);
    if (origensPermitidas.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`Origem não permitida: ${origin}`));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

Passo 10: Automatizar Testes com Node.js e supertest

A grande vantagem de testar APIs Node.js com Node.js é a possibilidade de criar uma suite de testes de segurança automatizados para integrar no pipeline de CI/CD. Com supertest e jest, pode testar automaticamente os principais vetores de ataque em cada pull request.

// seguranca.test.js
const request = require('supertest');
const app = require('./app');

describe('Testes de Segurança - OWASP API Top 10 2023', () => {
  let tokenAlice;

  beforeAll(async () => {
    const res = await request(app)
      .post('/login')
      .send({ email: '[email protected]', senha: 'senha123' });
    tokenAlice = res.body.token;
  });

  // API1: BOLA - Alice não deve ver dados de outros utilizadores
  test('BOLA: acesso a recurso de terceiro retorna 403', async () => {
    const res = await request(app)
      .get('/utilizadores/2')
      .set('Authorization', `Bearer ${tokenAlice}`);
    expect(res.status).toBe(403);
    expect(res.body.senha).toBeUndefined();
  });

  // API2: Tokens JWT inválidos são rejeitados
  test('JWT inválido retorna 401', async () => {
    const res = await request(app)
      .get('/utilizadores/1')
      .set('Authorization', 'Bearer token.invalido.aqui');
    expect(res.status).toBe(401);
  });

  // API2: Token expirado é rejeitado
  test('JWT expirado retorna 401', async () => {
    const jwt = require('jsonwebtoken');
    const tokenExpirado = jwt.sign(
      { userId: 1 },
      process.env.JWT_SECRET,
      { expiresIn: '-1s' }
    );
    const res = await request(app)
      .get('/utilizadores/1')
      .set('Authorization', `Bearer ${tokenExpirado}`);
    expect(res.status).toBe(401);
  });

  // API3: A senha nunca é devolvida nos endpoints de perfil
  test('Senha nunca incluída na resposta', async () => {
    const res = await request(app)
      .get('/utilizadores/1')
      .set('Authorization', `Bearer ${tokenAlice}`);
    expect(res.body.senha).toBeUndefined();
    expect(res.body.password).toBeUndefined();
  });

  // API4: Rate limiting ativo no endpoint de login
  test('Rate limiting bloqueia após múltiplas tentativas', async () => {
    const tentativas = Array(20).fill(null).map(() =>
      request(app).post('/login').send({ email: '[email protected]', senha: 'errada' })
    );
    const resultados = await Promise.all(tentativas);
    const bloqueados = resultados.filter(r => r.status === 429);
    expect(bloqueados.length).toBeGreaterThan(0);
  });

  // API8: Cabeçalhos de segurança obrigatórios
  test('Cabeçalhos de segurança presentes na resposta', async () => {
    const res = await request(app)
      .get('/utilizadores/1')
      .set('Authorization', `Bearer ${tokenAlice}`);
    expect(res.headers['x-content-type-options']).toBe('nosniff');
    expect(res.headers['x-frame-options']).toBeDefined();
    expect(res.headers['x-powered-by']).toBeUndefined();
  });

  // SQL Injection: payloads comuns não devem causar erros 500
  test('Payloads SQL Injection tratados sem erro 500', async () => {
    const payloads = [
      "' OR '1'='1",
      "'; DROP TABLE utilizadores; --",
      "1 UNION SELECT * FROM utilizadores",
      "' AND SLEEP(5) --",
    ];
    for (const payload of payloads) {
      const res = await request(app)
        .post('/pesquisar')
        .send({ q: payload });
      expect(res.status).not.toBe(500);
    }
  });
});

Execute os testes com:

npx jest seguranca.test.js --verbose

# Output esperado (API corrigida):
# PASS seguranca.test.js
#   Testes de Segurança - OWASP API Top 10 2023
#     ✓ BOLA: acesso a recurso de terceiro retorna 403 (23ms)
#     ✓ JWT inválido retorna 401 (8ms)
#     ✓ JWT expirado retorna 401 (15ms)
#     ✓ Senha nunca incluída na resposta (11ms)
#     ✓ Rate limiting bloqueia após múltiplas tentativas (234ms)
#     ✓ Cabeçalhos de segurança presentes na resposta (9ms)
#     ✓ Payloads SQL Injection tratados sem erro 500 (45ms)
#
# Tests: 7 passed, 7 total

Passo 11: Análise de Dependências com npm audit e Snyk

Uma API Node.js moderna depende de dezenas de pacotes npm. Cada um é uma potencial superfície de ataque. O Snyk State of Open Source Security 2025 revela que 60% dos projetos Node.js têm pelo menos uma dependência com vulnerabilidade conhecida. O artigo npm audit: 12 Steps to Fix Node.js Vulnerabilities cobre este tema em detalhe.

# Auditoria básica com npm
npm audit

# Output:
# found 3 vulnerabilities (1 moderate, 2 high)

# Auditoria detalhada com formato JSON
npm audit --json | python3 -c "
import json, sys
data = json.load(sys.stdin)
vulns = data.get('vulnerabilities', {})
for name, info in vulns.items():
    sev = info.get('severity', 'unknown')
    print(f'{sev.upper():10} {name}')
"

# HIGH       express
# HIGH       jsonwebtoken
# MODERATE   cookie

# Correção automática sem breaking changes
npm audit fix

# Snyk para análise mais profunda
npm install -g snyk
snyk auth
snyk test

# Integrar no CI/CD - bloquear deploy com vulnerabilidades HIGH
# Adicionar ao package.json:
# "scripts": {
#   "pretest": "npm audit --audit-level=high"
# }

Passo 12: Documentar Vulnerabilidades e Gerar Relatório

O relatório final é o produto mais valioso de um exercício de penetration testing. Um bom relatório permite à equipa de desenvolvimento reproduzir e corrigir cada vulnerabilidade sem ambiguidade.

Para cada vulnerabilidade encontrada, documente com o seguinte formato:

## VULN-001: Broken Object Level Authorization (BOLA)
**Severidade**: Crítica
**Categoria OWASP**: API1:2023
**Endpoint**: GET /utilizadores/:id

### Reprodução
1. Autenticar como [email protected] via POST /login
2. Usar o token recebido para GET /utilizadores/3
3. A API retorna dados do utilizador 3 (admin) sem verificação

### Impacto
Qualquer utilizador autenticado pode aceder aos dados de qualquer outro
utilizador, incluindo emails, senhas em texto simples e estado admin.

### Prova de Conceito
curl -H "Authorization: Bearer TOKEN_ALICE" http://api/utilizadores/3
Resposta: {"id":3,"email":"[email protected]","senha":"admin999","admin":true}

### Remediação
Adicionar verificação:
if (req.params.id !== req.utilizador.userId && !req.utilizador.admin) {
  return res.status(403).json({ erro: 'Acesso negado' });
}

### Referências
- OWASP API1:2023: https://owasp.org/API-Security/
- CWE-639: Authorization Bypass Through User-Controlled Key

Inclua no relatório final: sumário executivo para gestão não técnica, metodologia usada, inventário de endpoints testados, lista de vulnerabilidades com severidade, prova de conceito para cada vulnerabilidade, impacto de negócio estimado e recomendações de remediação priorizadas.

5 Armadilhas Comuns em Pentest de APIs Node.js

Armadilha 1: Testar apenas os endpoints documentados. As APIs têm frequentemente endpoints legacy, endpoints de debug (/debug, /health, /_internal) ou versões antigas (/v1, /v0) não documentados mas ainda ativos. Use sempre uma wordlist para descobrir endpoints ocultos com gobuster ou ffuf.

Armadilha 2: Ignorar a lógica de negócio. O OWASP ZAP e SQLmap não detetam vulnerabilidades de lógica de negócio: transferências de valor negativo, cupões de desconto infinitos, ou cancelamentos de pedidos alheios. Estas vulnerabilidades exigem testes manuais específicos para cada fluxo de negócio crítico.

Armadilha 3: Confundir autenticação com autorização. Um endpoint pode verificar corretamente que o utilizador está autenticado (JWT válido) mas não verificar se tem permissão para aceder ao recurso específico. BOLA e BFLA acontecem exatamente neste ponto cego. Teste cada endpoint com utilizadores de diferentes perfis e IDs diferentes.

Armadilha 4: Testar em produção sem janela de manutenção. Testes de carga para rate limiting, ou scans com SQLmap, podem causar indisponibilidade em produção. Defina sempre um ambiente de staging idêntico ao de produção para testes intrusivos.

Armadilha 5: Ignorar respostas de erro detalhadas. APIs que retornam stack traces, nomes de colunas de base de dados ou versões de frameworks nos erros fornecem informação valiosa a um atacante. Verifique que em produção os erros retornam mensagens genéricas:

// MAU: Expõe detalhes internos
app.use((err, req, res, next) => {
  res.status(500).json({ erro: err.message, stack: err.stack });
});

// BOM: Mensagem genérica com ID de referência para logs internos
app.use((err, req, res, next) => {
  const refId = require('crypto').randomUUID();
  console.error(`[${refId}]`, err); // Log completo apenas no servidor
  res.status(500).json({ erro: 'Erro interno do servidor', refId });
});

Resolução de Problemas: 8 Situações Frequentes

Problema 1: OWASP ZAP não encontra endpoints da API. Causa: o ZAP usa spider HTML por padrão e APIs REST não têm links HTML. Solução: use o API Spider do ZAP ou importe diretamente um ficheiro OpenAPI 3.0 via Import > Import an OpenAPI Definition from URL.

Problema 2: SQLmap retorna "parameters do not appear to be injectable". Causa: a API usa ORM com queries parametrizadas, ou há um WAF a bloquear os payloads. Solução: aumente o nível e o risco (--level=5 --risk=3) e use técnicas de tamper (--tamper=space2comment,randomcase). Resultado sem injeção é geralmente sinal de proteção adequada.

Problema 3: Testes supertest falham com ECONNREFUSED. Causa: o servidor Express não está exportado corretamente. Solução: separe o app do servidor. Exporte o app sem chamar app.listen() no ficheiro principal, e use um ficheiro separado server.js apenas para iniciar o servidor em produção.

Problema 4: Rate limiting bloqueia a suite de testes. Causa: rate limiting ativo no ambiente de teste. Solução: use NODE_ENV=test para aumentar os limites durante os testes: max: process.env.NODE_ENV === 'test' ? 10000 : 10.

Problema 5: Burp Suite não interceta pedidos da API mobile. Causa: certificate pinning ou proxy não configurado. Solução: instale o certificado CA do Burp no dispositivo e configure o proxy manualmente para 127.0.0.1:8080. Para certificate pinning, use Frida para bypass apenas em ambientes de teste autorizados.

Problema 6: npm audit não encontra vulnerabilidades óbvias. Causa: a base de dados do npm Advisory pode ter menos cobertura que outras bases. Solução: combine npm audit com Snyk ou OWASP Dependency-Check para cobertura máxima.

Problema 7: CORS parece correto mas a origem ainda é refletida. Causa: o middleware CORS está configurado depois de outro middleware que define cabeçalhos manualmente. Solução: certifique-se de que o middleware CORS é o primeiro registado, antes de qualquer rota ou middleware que manipule cabeçalhos de resposta.

Problema 8: O ataque JWT alg:none é rejeitado mas outros ataques JWT funcionam. Causa: a biblioteca jsonwebtoken em versões recentes rejeita none por padrão, mas pode ainda ser vulnerável a confusão de algoritmos RS256 vs HS256. Solução: especifique sempre o algoritmo aceite na verificação: jwt.verify(token, segredo, { algorithms: ['HS256'] }).

Dicas Avançadas: Fuzzing e Testes Baseados em Propriedades

Para equipas que querem segurança integrada no ciclo de desenvolvimento, o fuzzing e os testes baseados em propriedades aumentam significativamente a cobertura sem adicionar muita complexidade ao pipeline.

Fuzzing de APIs com Inputs Aleatórios

O fuzzing envia inputs inesperados para encontrar comportamentos anómalos que os testes estruturados não cobrem. Um fuzzer simples em Node.js para detetar erros 500 inesperados:

// fuzz-api.js
const request = require('supertest');
const app = require('./app');

const payloadsFuzz = [
  null, '', ' ', '\n', '\t', '\0',
  0, -1, 999999999, NaN,
  'a'.repeat(10000),            // Input gigante
  '',  // XSS
  '../../../etc/passwd',        // Path traversal
  '{{7*7}}', '${7*7}',         // Template injection
  '{"nested":{"deeply":{}}}',  // JSON complexo
];

async function fuzzEndpoint(endpoint, campo) {
  const problemas = [];
  for (const payload of payloadsFuzz) {
    const res = await request(app)
      .post(endpoint)
      .send({ [campo]: payload })
      .catch(e => ({ status: 0, error: e.message }));

    if (res.status === 500) {
      problemas.push({ payload, status: 500 });
    }
  }
  return problemas;
}

(async () => {
  const problemas = await fuzzEndpoint('/pesquisar', 'q');
  if (problemas.length > 0) {
    console.error('Erros 500 encontrados durante fuzzing:');
    problemas.forEach(p => console.error(p));
    process.exit(1);
  } else {
    console.log('Fuzzing concluído sem erros 500 inesperados');
  }
})();

Dica: Testes Baseados em Propriedades com fast-check. Com a biblioteca fast-check, pode verificar invariantes de segurança com centenas de casos gerados automaticamente:

npm install --save-dev fast-check

// seguranca-propriedades.test.js
const fc = require('fast-check');
const request = require('supertest');
const app = require('./app');

test('Qualquer input em /pesquisar nunca retorna HTTP 500', async () => {
  await fc.assert(
    fc.asyncProperty(fc.string(), async (input) => {
      const res = await request(app)
        .post('/pesquisar')
        .send({ q: input });
      return res.status !== 500;
    }),
    { numRuns: 200 }
  );
}, 30000);

test('Endpoint /utilizadores/:id nunca expõe senha', async () => {
  await fc.assert(
    fc.asyncProperty(fc.integer({ min: 1, max: 100 }), async (id) => {
      const res = await request(app)
        .get(`/utilizadores/${id}`)
        .set('Authorization', `Bearer ${tokenValido}`);
      return res.body.senha === undefined &&
             res.body.password === undefined;
    }),
    { numRuns: 50 }
  );
});

Perguntas Frequentes sobre Pentest de APIs Node.js

Um pentester precisa de acesso ao código-fonte da API para testar?

Não é obrigatório. Os testes de caixa negra (sem código-fonte) são os mais representativos de um ataque real. Porém, os testes de caixa branca (com código-fonte) são mais eficientes e completos, detetando mais vulnerabilidades em menos tempo. Para auditorias internas, prefira sempre testes de caixa branca ou cinzenta (com documentação da API mas sem código).

Com que frequência devo fazer pentest à minha API Node.js?

O mínimo recomendado é uma vez por trimestre, ou sempre que houver uma alteração significativa na arquitetura (novo endpoint, novo sistema de autenticação, integração com terceiros). Para APIs em setores regulados como fintech, saúde ou governo, a frequência mínima é mensal. Os testes automatizados descritos no Passo 10 devem correr em cada pull request.

Qual a diferença entre um pentest manual e um scan automatizado?

Scanners automatizados como o OWASP ZAP cobrem rapidamente centenas de vetores conhecidos, mas têm taxas de falsos positivos elevadas e não detetam vulnerabilidades de lógica de negócio. Um pentester manual complementa os scanners ao analisar contexto, encadear vulnerabilidades (ex: SSRF + BOLA) e explorar fluxos de negócio específicos. A combinação de ambos é mais eficaz que qualquer um isolado.

O que é o BOLA e por que é o risco número 1 para APIs?

BOLA (Broken Object Level Authorization) ocorre quando uma API não verifica se o utilizador autenticado tem permissão para aceder a um objeto específico (identificado por ID na URL). É o risco número 1 porque é ubíquo em APIs REST (qualquer endpoint com IDs é potencialmente vulnerável) e difícil de detetar com scanners automáticos, pois requer compreensão da lógica de autorização da aplicação.

Posso usar SQLmap sem ser pentester profissional?

Sim, em sistemas que possui ou tem autorização escrita para testar. O SQLmap é uma ferramenta de auditoria legítima. Em Portugal, o uso de ferramentas de pentest em sistemas alheios sem autorização é crime ao abrigo do Código Penal. Nunca use SQLmap ou qualquer ferramenta de pentest em sistemas de terceiros sem autorização expressa e documentada.

Como protejo a minha API Node.js contra ataques GraphQL?

Para APIs GraphQL, as proteções essenciais incluem: desativar a introspection em produção, limitar a profundidade máxima das queries, implementar query cost analysis para prevenir queries caras, e usar persisted queries em vez de queries arbitrárias. Bibliotecas como graphql-depth-limit e graphql-cost-analysis facilitam estas proteções no ecossistema Node.js.

Qual o custo de contratar um pentester externo para APIs Node.js em Portugal?

Em Portugal, um exercício de pentest a uma API de tamanho médio (10 a 30 endpoints) custa entre €1.500 e €5.000, dependendo da complexidade e do nível de certificação do pentester (CEH, OSCP, GWAPT). Para startups, os programas de bug bounty em plataformas como HackerOne ou Intigriti são uma alternativa mais económica, pagando apenas por vulnerabilidades reais encontradas.

O que devo incluir num relatório de pentest de API?

Um relatório completo deve incluir: sumário executivo (para gestão não técnica), metodologia usada, inventário de endpoints testados, lista de vulnerabilidades com severidade CVSS, prova de conceito para cada vulnerabilidade, impacto de negócio estimado, e recomendações de remediação priorizadas. Um relatório sem prova de conceito não permite à equipa técnica reproduzir e confirmar a vulnerabilidade encontrada.

Cobertura Relacionada

Artigos Relacionados

Referências Externas