Sumário
- Por que Segurança Backend é uma Habilidade Obrigatória
- OWASP API Security Top 10 — 2023
- Autenticação Robusta: JWT Best Practices
- OAuth2 e OpenID Connect: Autenticação de Terceiros
- Multi-Factor Authentication (MFA/TOTP)
- Proteção contra Injeção: SQL, NoSQL e Command Injection
- Cross-Site Scripting (XSS) e Content Security Policy
- CSRF: Cross-Site Request Forgery
- Rate Limiting e Proteção contra DDoS
- Criptografia: Hashing, Encryption e Secrets Management
- HTTPS, TLS e Certificados
- Headers de Segurança HTTP
- Autorização: RBAC e ABAC
- Auditoria, Logging e Detecção de Intrusão
- Secrets Management e Configuração Segura
- Security Testing: SAST, DAST e Penetration Testing
- Conclusão: Security by Design
1. Por que Segurança Backend é uma Habilidade Obrigatória {#introducao}
Em 2024, o custo médio global de uma violação de dados atingiu US$ 4,88 milhões, segundo o relatório anual da IBM. Não é mais uma questão de “se” uma aplicação será atacada, mas “quando”. APIs são o vetor de ataque preferido de hackers porque são expostas publicamente, processam dados sensíveis e frequentemente têm autenticação e autorização inadequadas.
Mais alarmante: 94% das aplicações têm ao menos uma vulnerabilidade crítica de segurança na produção, segundo dados do SANS Institute. A maioria dessas vulnerabilidades não são exóticas — são as mesmas listadas no OWASP Top 10 há anos, evitáveis com boas práticas conhecidas.
A Mentalidade de Security by Design
Segurança não pode ser uma afterthought. Adicionar segurança no final do desenvolvimento é como tentar fortalecer uma casa depois de construída — possível, mas muito mais caro e menos eficaz.
Security by Design significa:
- Modelar ameaças (threat modeling) durante o design
- Seguir o princípio de least privilege (mínimo privilégio)
- Validar toda entrada, independente da origem
- Nunca confiar no cliente — sempre validar no servidor
- Defense in depth: múltiplas camadas de proteção
- Falhar seguro (fail secure, not fail open)
O Custo de Ignorar Segurança
Violações reais que custaram caro:
- Facebook (2019): 540 milhões de registros expostos por API mal configurada — US$ 5B em multa
- Equifax (2017): 147 milhões de SSNs expostos por falha em autenticação — US$ 575M em settlement
- Twitch (2021): Código-fonte e dados financeiros vazados por S3 mal configurado
- LastPass (2022): Cofres de senhas de 33 milhões de usuários roubados
Esses casos não foram causados por hackers sofisticados explorando zero-days. Foram APIs sem autenticação, segredos em código, tokens sem expiração, S3 buckets públicos — erros básicos.
2. OWASP API Security Top 10 — 2023 {#owasp}
A OWASP API Security Top 10 é o guia definitivo para as vulnerabilidades mais comuns e críticas em APIs. Vamos cobrir cada uma com exemplos práticos.
API1:2023 — Broken Object Level Authorization (BOLA)
A vulnerabilidade mais prevalente. O servidor não verifica se o usuário autenticado tem permissão de acessar o objeto específico solicitado.
// ❌ VULNERÁVEL: qualquer usuário autenticado pode ver qualquer pedido
router.get('/orders/:id', authenticate, async (req, res) => {
const order = await orderRepository.findById(req.params.id);
if (!order) return res.status(404).json({ message: 'Não encontrado' });
return res.json(order); // ← Sem verificar se o pedido pertence ao usuário!
});
// ✅ SEGURO: verificar se o recurso pertence ao usuário
router.get('/orders/:id', authenticate, async (req, res) => {
const order = await orderRepository.findOne({
where: {
id: req.params.id,
userId: req.user!.userId, // ← SEMPRE filtrar pelo usuário autenticado
},
});
if (!order) {
// Retornar 404 mesmo quando existe mas não pertence ao usuário
// (não revelar a existência do recurso)
return res.status(404).json({ message: 'Pedido não encontrado' });
}
return res.json(order);
});
// Middleware reutilizável para BOLA check
export function ownershipGuard(
resourceFetcher: (id: string) => Promise<{ userId: string } | null>
) {
return async (req: Request, res: Response, next: NextFunction) => {
const resource = await resourceFetcher(req.params.id);
if (!resource) {
return res.status(404).json({ message: 'Recurso não encontrado' });
}
// Admin pode ver qualquer recurso
if (req.user!.role === 'ADMIN') return next();
if (resource.userId !== req.user!.userId) {
return res.status(403).json({ message: 'Acesso negado' });
}
next();
};
}
API2:2023 — Broken Authentication
// ❌ VULNERÁVEL: senha fraca, sem rate limiting, sem bloqueio
router.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await userRepository.findByEmail(email);
if (user && user.password === password) { // ← Senha em plaintext!
const token = jwt.sign({ id: user.id }, 'secret'); // ← Secret fraco!
return res.json({ token });
}
return res.status(401).json({ message: 'Inválido' });
});
// ✅ SEGURO: implementação completa
export class SecureAuthService {
private readonly FAILED_ATTEMPTS_KEY = (email: string) => `failed_login:${email}`;
private readonly MAX_ATTEMPTS = 5;
private readonly LOCKOUT_DURATION = 15 * 60; // 15 minutos
async login(email: string, password: string, ip: string) {
const lockKey = this.FAILED_ATTEMPTS_KEY(email);
// Verificar se conta está bloqueada
const attempts = await redis.get(lockKey);
if (parseInt(attempts || '0') >= this.MAX_ATTEMPTS) {
const ttl = await redis.ttl(lockKey);
throw new AppError(
`Conta temporariamente bloqueada. Tente em ${Math.ceil(ttl / 60)} minutos.`,
429
);
}
// Usar tempo constante para prevenir timing attacks
const user = await userRepository.findByEmail(email.toLowerCase());
// Hash fictício para manter tempo constante mesmo quando usuário não existe
const dummyHash = '$2b$12$dummy.hash.to.prevent.timing.attacks.here';
const passwordToCheck = user?.password || dummyHash;
// bcrypt.compare sempre leva ~100ms independente do resultado
const isValid = await bcrypt.compare(password, passwordToCheck);
if (!user || !isValid) {
// Incrementar falhas com TTL
await redis.incr(lockKey);
await redis.expire(lockKey, this.LOCKOUT_DURATION);
// Log de segurança
await securityLogger.logFailedLogin(email, ip);
// Mensagem genérica (não revelar se email existe)
throw new AppError('Credenciais inválidas', 401);
}
// Reset contador de falhas após login bem-sucedido
await redis.del(lockKey);
return this.generateTokens(user);
}
}
API3:2023 — Broken Object Property Level Authorization
// ❌ VULNERÁVEL: Mass Assignment — atualizar qualquer campo
router.put('/users/:id', authenticate, async (req, res) => {
const user = await userRepository.update(req.params.id, req.body);
// ← req.body pode conter { role: 'ADMIN', isVerified: true }!
return res.json(user);
});
// ✅ SEGURO: whitelist explícita dos campos permitidos
router.put('/users/:id', authenticate, async (req, res) => {
// Apenas campos permitidos para usuários normais
const allowedFields: (keyof UpdateUserDTO)[] = ['name', 'bio', 'avatarUrl'];
// Admins podem atualizar mais campos
if (req.user!.role === 'ADMIN') {
allowedFields.push('role', 'status');
}
const sanitizedData = pick(req.body, allowedFields);
if (Object.keys(sanitizedData).length === 0) {
return res.status(400).json({ message: 'Nenhum campo válido para atualizar' });
}
const user = await userRepository.update(req.params.id, sanitizedData);
// Retornar apenas campos seguros (não retornar password, tokens, etc.)
const { password, refreshToken, ...safeUser } = user;
return res.json(safeUser);
});
function pick(obj: T, keys: (keyof T)[]): Partial {
return keys.reduce((acc, key) => {
if (key in obj) acc[key] = obj[key];
return acc;
}, {} as Partial);
}
API4:2023 — Unrestricted Resource Consumption
// ❌ VULNERÁVEL: sem limites de upload, paginação, ou processamento
router.post('/products/import', async (req, res) => {
const products = req.body.products; // Pode ter 1 milhão de produtos!
await productService.importAll(products);
return res.json({ imported: products.length });
});
// ✅ SEGURO: limites em todos os recursos consumíveis
router.post('/products/import',
authenticate,
multer({
limits: {
fileSize: 10 * 1024 * 1024, // Máximo 10MB
files: 1,
}
}).single('file'),
async (req, res) => {
const file = req.file;
if (!file) return res.status(400).json({ message: 'Arquivo não fornecido' });
// Limitar linhas processadas
const MAX_RECORDS = 10000;
const records = parseCSV(file.buffer).slice(0, MAX_RECORDS);
// Processar em background (não bloquear a requisição)
const jobId = await importQueue.add('import-products', {
records,
userId: req.user!.userId,
});
return res.status(202).json({
message: `Importação iniciada. ${records.length} registros serão processados.`,
jobId,
statusUrl: `/jobs/${jobId}`,
});
}
);
// Limites de paginação
router.get('/products', async (req, res) => {
const page = Math.max(1, parseInt(req.query.page as string) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string) || 20));
// ↑ Máximo de 100 itens por página, nunca zero
const products = await productRepository.findAll({ page, limit });
return res.json(products);
});
3. Autenticação Robusta: JWT Best Practices {#jwt}
Os Erros Mais Comuns com JWT
// ❌ 1. Algorithm confusion attack: nunca usar 'none' ou aceitar qualquer algoritmo
const token = jwt.verify(token, secret);
// ↑ Sem especificar algorithms, atacante pode trocar para 'none'!
// ✅ SEGURO: sempre especificar o algoritmo explicitamente
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'], // Apenas este algoritmo
issuer: 'minha-api',
audience: 'clientes-api',
});
// ❌ 2. Secret fraco
const JWT_SECRET = 'secret'; // Trivialmente brute-forceable
// ✅ SEGURO: secret com alta entropia (mínimo 256 bits = 32 bytes random)
// Gerar com: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
const JWT_SECRET = process.env.JWT_SECRET; // 128 chars hex = 64 bytes = 512 bits
// ❌ 3. Token sem expiração
const token = jwt.sign({ userId }, secret); // Sem expiresIn!
// ✅ SEGURO: access token curto + refresh token com rotação
const accessToken = jwt.sign({ userId, role }, secret, {
expiresIn: '15m', // Access token: apenas 15 minutos
issuer: 'minha-api',
audience: 'clientes-api',
jwtid: crypto.randomUUID(), // JTI: ID único para blacklisting
});
// ❌ 4. Dados sensíveis no payload (JWT é encodado, NÃO encriptado!)
const token = jwt.sign({
userId,
password, // ← Isso fica visível para qualquer um!
creditCard, // ← NUNCA fazer isso!
}, secret);
// ✅ SEGURO: apenas identificadores no payload
const token = jwt.sign({
sub: userId, // Subject (RFC 7519)
role: user.role, // Apenas o necessário
jti: uuid(), // JWT ID para blacklisting
}, secret);
Implementação Completa de JWT com Blacklisting
// src/modules/auth/jwt.service.ts
import jwt, { JwtPayload, SignOptions } from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { redis } from '@/config/redis';
import { env } from '@/config/env';
import { AppError } from '@/shared/errors/AppError';
interface TokenPayload {
sub: string; // userId
role: string;
jti: string; // JWT ID (único por token)
iat: number;
exp: number;
}
const JWT_BLACKLIST_PREFIX = 'jwt:blacklist:';
export class JwtService {
generateAccessToken(userId: string, role: string): string {
const jti = uuidv4();
return jwt.sign(
{ sub: userId, role, jti },
env.JWT_SECRET,
{
algorithm: 'HS256',
expiresIn: '15m',
issuer: env.JWT_ISSUER || 'api',
audience: env.JWT_AUDIENCE || 'api-clients',
} as SignOptions
);
}
generateRefreshToken(userId: string): string {
return jwt.sign(
{ sub: userId, jti: uuidv4(), type: 'refresh' },
env.JWT_REFRESH_SECRET,
{
algorithm: 'HS256',
expiresIn: '30d',
} as SignOptions
);
}
verifyAccessToken(token: string): TokenPayload {
try {
const payload = jwt.verify(token, env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: env.JWT_ISSUER || 'api',
audience: env.JWT_AUDIENCE || 'api-clients',
complete: false,
}) as TokenPayload;
return payload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new AppError('Token expirado', 401);
}
if (error instanceof jwt.JsonWebTokenError) {
throw new AppError('Token inválido', 401);
}
throw new AppError('Erro de autenticação', 401);
}
}
async verifyAndCheckBlacklist(token: string): Promise {
const payload = this.verifyAccessToken(token);
// Verificar se o token foi revogado (blacklist)
const isBlacklisted = await redis.exists(`${JWT_BLACKLIST_PREFIX}${payload.jti}`);
if (isBlacklisted) {
throw new AppError('Token revogado', 401);
}
return payload;
}
async revokeToken(token: string): Promise {
try {
const payload = jwt.decode(token) as TokenPayload;
if (!payload?.jti || !payload?.exp) return;
// Blacklist até a expiração original do token
const ttl = payload.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.setex(`${JWT_BLACKLIST_PREFIX}${payload.jti}`, ttl, '1');
}
} catch {
// Ignorar erros ao revogar tokens inválidos
}
}
async revokeAllUserTokens(userId: string): Promise {
// Incrementar a "geração" do usuário
// Tokens com geração antiga são considerados inválidos
const generation = await redis.incr(`user:token:generation:${userId}`);
await redis.expire(`user:token:generation:${userId}`, 86400 * 30);
console.log(`Tokens do usuário ${userId} revogados (geração ${generation})`);
}
}
export const jwtService = new JwtService();
4. OAuth2 e OpenID Connect {#oauth2}
// src/modules/auth/oauth.service.ts
import { env } from '@/config/env';
import { AppError } from '@/shared/errors/AppError';
import { redis } from '@/config/redis';
import crypto from 'crypto';
interface OAuthProvider {
clientId: string;
clientSecret: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
scope: string;
}
const PROVIDERS: Record = {
google: {
clientId: env.GOOGLE_CLIENT_ID!,
clientSecret: env.GOOGLE_CLIENT_SECRET!,
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
scope: 'openid email profile',
},
github: {
clientId: env.GITHUB_CLIENT_ID!,
clientSecret: env.GITHUB_CLIENT_SECRET!,
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
scope: 'read:user user:email',
},
};
export class OAuthService {
/**
* Gerar URL de autorização com PKCE e state
* PKCE (Proof Key for Code Exchange) previne authorization code interception
*/
async generateAuthUrl(provider: string, redirectUri: string): Promise<{
url: string;
state: string;
codeVerifier: string;
}> {
const config = PROVIDERS[provider];
if (!config) throw new AppError(`Provider '${provider}' não suportado`, 400);
// State: CSRF protection para OAuth
const state = crypto.randomBytes(32).toString('hex');
// PKCE: code verifier e challenge
const codeVerifier = crypto.randomBytes(64).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Armazenar state + code_verifier para verificação posterior (5 minutos)
await redis.setex(
`oauth:state:${state}`,
300,
JSON.stringify({ codeVerifier, provider, redirectUri })
);
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: config.scope,
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
access_type: 'offline', // Google: para receber refresh_token
prompt: 'consent',
});
return {
url: `${config.authorizationUrl}?${params.toString()}`,
state,
codeVerifier,
};
}
/**
* Trocar authorization code por tokens
*/
async handleCallback(
provider: string,
code: string,
state: string,
redirectUri: string
): Promise<{ user: any; isNewUser: boolean }> {
const config = PROVIDERS[provider];
if (!config) throw new AppError('Provider inválido', 400);
// Verificar e recuperar state (CSRF protection)
const stateData = await redis.get(`oauth:state:${state}`);
if (!stateData) {
throw new AppError('State inválido ou expirado. Inicie o login novamente.', 400);
}
const { codeVerifier, provider: storedProvider } = JSON.parse(stateData);
if (storedProvider !== provider) {
throw new AppError('Provider não corresponde ao state', 400);
}
// Invalidar state imediatamente (one-time use)
await redis.del(`oauth:state:${state}`);
// Trocar code por access token com PKCE
const tokenResponse = await fetch(config.tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: config.clientId,
client_secret: config.clientSecret,
code_verifier: codeVerifier, // PKCE verification
}),
});
if (!tokenResponse.ok) {
throw new AppError('Falha ao trocar código por token', 400);
}
const tokens = await tokenResponse.json();
if (tokens.error) {
throw new AppError(`OAuth error: ${tokens.error_description}`, 400);
}
// Buscar informações do usuário
const userInfo = await this.fetchUserInfo(
config.userInfoUrl,
tokens.access_token,
provider
);
// Criar ou atualizar usuário no banco
return this.upsertOAuthUser(provider, userInfo, tokens);
}
private async fetchUserInfo(
userInfoUrl: string,
accessToken: string,
provider: string
): Promise {
const response = await fetch(userInfoUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
...(provider === 'github' ? { 'User-Agent': 'MyApp/1.0' } : {}),
},
});
if (!response.ok) {
throw new AppError('Falha ao buscar informações do usuário', 400);
}
const userInfo = await response.json();
// Para GitHub, buscar email primário separadamente se não vier no userInfo
if (provider === 'github' && !userInfo.email) {
const emailResponse = await fetch('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'User-Agent': 'MyApp/1.0',
},
});
const emails = await emailResponse.json();
userInfo.email = emails.find((e: any) => e.primary)?.email;
}
return userInfo;
}
private async upsertOAuthUser(
provider: string,
userInfo: any,
tokens: any
): Promise<{ user: any; isNewUser: boolean }> {
const email = userInfo.email?.toLowerCase();
if (!email) {
throw new AppError('Não foi possível obter o email do provider', 400);
}
// Verificar se usuário já existe
let user = await userRepository.findByEmail(email);
let isNewUser = false;
if (!user) {
// Criar novo usuário
user = await userRepository.create({
email,
name: userInfo.name || userInfo.login || email.split('@')[0],
username: await this.generateUniqueUsername(userInfo.login || email.split('@')[0]),
provider,
providerId: userInfo.id || userInfo.sub,
avatarUrl: userInfo.picture || userInfo.avatar_url,
emailVerified: true, // Email verificado pelo provider
password: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), 12),
});
isNewUser = true;
} else {
// Atualizar info do provider se necessário
await userRepository.update(user.id, {
avatarUrl: userInfo.picture || userInfo.avatar_url || user.avatarUrl,
lastLoginAt: new Date(),
});
}
return { user, isNewUser };
}
private async generateUniqueUsername(base: string): Promise {
const sanitized = base.toLowerCase().replace(/[^a-z0-9_-]/g, '');
let username = sanitized;
let counter = 1;
while (await userRepository.findByUsername(username)) {
username = `${sanitized}${counter++}`;
}
return username;
}
}
5. Multi-Factor Authentication (MFA/TOTP) {#mfa}
pnpm add otpauth qrcode
pnpm add -D @types/qrcode
// src/modules/auth/mfa.service.ts
import * as OTPAuth from 'otpauth';
import QRCode from 'qrcode';
import crypto from 'crypto';
import { redis } from '@/config/redis';
import { AppError } from '@/shared/errors/AppError';
export class MFAService {
private readonly ISSUER = 'MinhaApp';
private readonly ALGORITHM = 'SHA1';
private readonly DIGITS = 6;
private readonly PERIOD = 30; // segundos
private readonly WINDOW = 1; // Aceitar ±1 período (tolerância de clock drift)
private readonly BACKUP_CODES_COUNT = 10;
async setupMFA(userId: string, username: string): Promise<{
secret: string;
qrCodeUrl: string;
backupCodes: string[];
}> {
// Gerar secret aleatório
const secret = new OTPAuth.Secret({ size: 20 });
// Criar TOTP
const totp = new OTPAuth.TOTP({
issuer: this.ISSUER,
label: username,
algorithm: this.ALGORITHM,
digits: this.DIGITS,
period: this.PERIOD,
secret,
});
// Gerar QR code URI
const otpAuthUrl = totp.toString();
const qrCodeUrl = await QRCode.toDataURL(otpAuthUrl);
// Gerar backup codes (para quando o dispositivo TOTP não está disponível)
const backupCodes = this.generateBackupCodes();
const hashedBackupCodes = await Promise.all(
backupCodes.map(code => bcrypt.hash(code, 10))
);
// Armazenar temporariamente (aguardando confirmação)
await redis.setex(
`mfa:setup:${userId}`,
600, // 10 minutos para confirmar
JSON.stringify({
secret: secret.base32,
backupCodes: hashedBackupCodes,
})
);
return {
secret: secret.base32,
qrCodeUrl,
backupCodes, // Mostrar UMA VEZ para o usuário salvar
};
}
async confirmMFASetup(userId: string, totpCode: string): Promise {
const setupData = await redis.get(`mfa:setup:${userId}`);
if (!setupData) {
throw new AppError('Setup de MFA não iniciado ou expirado', 400);
}
const { secret, backupCodes } = JSON.parse(setupData);
// Verificar o código TOTP
const isValid = this.verifyTOTP(secret, totpCode);
if (!isValid) {
throw new AppError('Código TOTP inválido', 400);
}
// Salvar no banco de dados
await userRepository.update(userId, {
mfaEnabled: true,
mfaSecret: this.encryptSecret(secret),
mfaBackupCodes: backupCodes,
});
// Limpar dados temporários
await redis.del(`mfa:setup:${userId}`);
}
async verifyMFA(userId: string, code: string): Promise {
const user = await userRepository.findById(userId);
if (!user?.mfaEnabled || !user.mfaSecret) {
throw new AppError('MFA não configurado para este usuário', 400);
}
const secret = this.decryptSecret(user.mfaSecret);
// Verificar TOTP normal
if (/^d{6}$/.test(code)) {
return this.verifyTOTP(secret, code);
}
// Verificar backup code
if (/^[a-f0-9]{8}-[a-f0-9]{4}$/.test(code)) {
return this.verifyAndConsumeBackupCode(userId, code, user.mfaBackupCodes);
}
return false;
}
private verifyTOTP(secret: string, token: string): boolean {
const totp = new OTPAuth.TOTP({
issuer: this.ISSUER,
algorithm: this.ALGORITHM,
digits: this.DIGITS,
period: this.PERIOD,
secret: OTPAuth.Secret.fromBase32(secret),
});
const delta = totp.validate({ token, window: this.WINDOW });
return delta !== null;
}
private async verifyAndConsumeBackupCode(
userId: string,
code: string,
hashedCodes: string[]
): Promise {
for (let i = 0; i < hashedCodes.length; i++) {
const isMatch = await bcrypt.compare(code, hashedCodes[i]);
if (isMatch) {
// Remover backup code usado (one-time use)
const updatedCodes = hashedCodes.filter((_, index) => index !== i);
await userRepository.update(userId, { mfaBackupCodes: updatedCodes });
if (updatedCodes.length === 0) {
// Avisar usuário que todos os backup codes foram usados
await notificationService.sendMFABackupCodesExhausted(userId);
}
return true;
}
}
return false;
}
private generateBackupCodes(): string[] {
return Array.from({ length: this.BACKUP_CODES_COUNT }, () =>
`${crypto.randomBytes(4).toString('hex')}-${crypto.randomBytes(2).toString('hex')}`
);
}
// Encriptar o secret TOTP armazenado no banco
private encryptSecret(secret: string): string {
const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(env.ENCRYPTION_KEY, 'salt', 32);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(secret, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return `${iv.toString('hex')}:${authTag}:${encrypted}`;
}
private decryptSecret(encryptedSecret: string): string {
const [ivHex, authTagHex, encrypted] = encryptedSecret.split(':');
const key = crypto.scryptSync(env.ENCRYPTION_KEY, 'salt', 32);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
6. Proteção contra Injeção {#injection}
SQL Injection via ORM (Prisma)
// ✅ Prisma usa queries parametrizadas automaticamente
// Isso é seguro:
const users = await prisma.user.findMany({
where: { email: req.body.email }, // ← Nunca haverá SQL injection aqui
});
// ❌ CUIDADO com raw queries!
// NUNCA fazer isso:
const result = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${req.body.email}'`
// ↑ VULNERÁVEL! Input: ' OR '1'='1
);
// ✅ Raw queries com parâmetros (quando necessário):
const result = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${req.body.email}
`;
// ↑ Template literal do Prisma é automaticamente parametrizado
// ✅ Para queries dinâmicas complexas:
import { Prisma } from '@prisma/client';
const result = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE email = ${req.body.email}`
);
NoSQL Injection (MongoDB/Mongoose)
// ❌ VULNERÁVEL a NoSQL injection
app.post('/login', async (req, res) => {
const { email, password } = req.body;
// Se email = { $gt: '' }, retorna TODOS os usuários!
const user = await User.findOne({ email });
});
// ✅ SEGURO: validar e sanitizar tipos
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(), // Força que seja uma string de email válida
password: z.string().min(1), // Força string
});
app.post('/login', async (req, res) => {
const { email, password } = loginSchema.parse(req.body);
// ↑ Zod rejeita { $gt: '' } porque não é uma string de email válida
const user = await User.findOne({ email });
});
// ✅ Sanitização adicional com express-mongo-sanitize
import mongoSanitize from 'express-mongo-sanitize';
app.use(mongoSanitize()); // Remove $ e . de req.body, req.params, req.query
Command Injection
import { exec } from 'child_process';
import path from 'path';
// ❌ VULNERÁVEL: entrada do usuário direto no shell
app.post('/convert', (req, res) => {
const filename = req.body.filename;
exec(`convert ${filename} output.pdf`);
// Se filename = 'file.docx; rm -rf /'... desastre!
});
// ✅ SEGURO: validar e sanitizar antes de qualquer operação de sistema
app.post('/convert', async (req, res) => {
// 1. Validar formato do nome do arquivo
const filenameSchema = z.string()
.min(1)
.max(255)
.regex(/^[a-zA-Z0-9._-]+$/, 'Nome de arquivo contém caracteres inválidos');
const filename = filenameSchema.parse(req.body.filename);
// 2. Normalizar e verificar que está dentro do diretório permitido
const uploadDir = path.resolve('/safe/uploads');
const filePath = path.resolve(uploadDir, filename);
if (!filePath.startsWith(uploadDir + path.sep)) {
throw new AppError('Path traversal detectado', 400);
}
// 3. Usar execFile em vez de exec (não usa shell, sem command injection)
const { execFile } = require('child_process');
execFile('convert', [filePath, 'output.pdf'], (error) => {
if (error) return res.status(500).json({ message: 'Erro na conversão' });
res.json({ success: true });
});
});
7. Cross-Site Scripting (XSS) e Content Security Policy {#xss}
// src/config/security.ts
import helmet from 'helmet';
import { Application } from 'express';
export function configureSecurityHeaders(app: Application): void {
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
useDefaults: true,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // APIs puras não servem CSS
imgSrc: ["'self'", 'data:', 'https:'],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
frameAncestors: ["'none'"], // Prevenir clickjacking
formAction: ["'self'"],
upgradeInsecureRequests: [],
blockAllMixedContent: [],
},
},
// HSTS: forçar HTTPS
hsts: {
maxAge: 31536000, // 1 ano
includeSubDomains: true,
preload: true,
},
// Prevenir MIME sniffing
noSniff: true,
// Prevenir clickjacking
frameguard: { action: 'deny' },
// Ocultar que usa Express
hidePoweredBy: true,
// XSS Protection (legado, CSP é melhor)
xssFilter: true,
// Referrer Policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
}
// Sanitização de output HTML (quando necessário renderizar HTML)
import DOMPurify from 'isomorphic-dompurify';
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'target'],
ALLOW_DATA_ATTR: false,
FORBID_SCRIPT: true,
});
}
8. CSRF Protection {#csrf}
// src/middlewares/csrf.middleware.ts
import csrf from 'csrf';
import { Request, Response, NextFunction } from 'express';
import { redis } from '@/config/redis';
import crypto from 'crypto';
const tokens = new csrf();
export class CSRFProtection {
// Double Submit Cookie pattern (stateless, sem sessão)
async generateToken(userId: string): Promise {
const secret = tokens.secretSync();
const token = tokens.create(secret);
// Armazenar secret no Redis (atrelado ao usuário)
await redis.setex(
`csrf:${userId}`,
3600, // 1 hora
secret
);
return token;
}
async validateToken(userId: string, token: string): Promise {
const secret = await redis.get(`csrf:${userId}`);
if (!secret) return false;
return tokens.verify(secret, token);
}
// Middleware para validar CSRF em requisições de mutação
middleware() {
return async (req: Request, res: Response, next: NextFunction) => {
// Apenas validar métodos de mutação
const mutationMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (!mutationMethods.includes(req.method)) return next();
// APIs puras com Content-Type JSON são protegidas por default
// (browsers não podem fazer cross-origin requests com Content-Type: application/json sem preflight)
if (req.headers['content-type']?.includes('application/json')) {
// Validar que o Origin/Referer é confiável
const origin = req.headers.origin || req.headers.referer;
if (origin && !this.isTrustedOrigin(origin)) {
return res.status(403).json({ message: 'CSRF: Origin não confiável' });
}
return next();
}
// Para form submissions (multipart, urlencoded): verificar token explicitamente
const csrfToken = req.headers['x-csrf-token'] || req.body?._csrf;
const userId = req.user?.userId;
if (!csrfToken || !userId) {
return res.status(403).json({ message: 'CSRF token ausente' });
}
const isValid = await this.validateToken(userId, csrfToken as string);
if (!isValid) {
return res.status(403).json({ message: 'CSRF token inválido' });
}
next();
};
}
private isTrustedOrigin(origin: string): boolean {
const trustedOrigins = (process.env.TRUSTED_ORIGINS || '').split(',');
return trustedOrigins.some(trusted => origin.startsWith(trusted.trim()));
}
}
9. Criptografia: Hashing, Encryption e Secrets Management {#criptografia}
// src/shared/crypto/crypto.service.ts
import crypto from 'crypto';
import { promisify } from 'util';
const scryptAsync = promisify(crypto.scrypt);
export class CryptoService {
/**
* Hash de senhas com bcrypt
* Usar APENAS bcrypt/argon2/scrypt para senhas (não SHA/MD5!)
*/
async hashPassword(password: string): Promise {
const saltRounds = parseInt(process.env.BCRYPT_ROUNDS || '12');
return bcrypt.hash(password, saltRounds);
}
async verifyPassword(password: string, hash: string): Promise {
return bcrypt.compare(password, hash);
}
/**
* Criptografia simétrica AES-256-GCM para dados sensíveis
* GCM fornece autenticação + criptografia (AEAD)
*/
async encrypt(plaintext: string): Promise {
const key = await this.deriveKey();
const iv = crypto.randomBytes(12); // 96 bits para GCM
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
ciphertext += cipher.final('base64');
const authTag = cipher.getAuthTag();
// Formato: iv:authTag:ciphertext (todos em base64)
return [
iv.toString('base64'),
authTag.toString('base64'),
ciphertext,
].join(':');
}
async decrypt(encrypted: string): Promise {
const [ivBase64, authTagBase64, ciphertext] = encrypted.split(':');
const key = await this.deriveKey();
const iv = Buffer.from(ivBase64, 'base64');
const authTag = Buffer.from(authTagBase64, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
private async deriveKey(): Promise {
const masterKey = process.env.ENCRYPTION_MASTER_KEY;
if (!masterKey) throw new Error('ENCRYPTION_MASTER_KEY não configurada');
return scryptAsync(masterKey, 'encryption-salt-v1', 32) as Promise;
}
/**
* Hash determinístico para dados que precisam ser buscados mas não expostos
* Exemplo: CPF, número de cartão mascarado para busca
*/
deterministicHash(value: string): string {
const hmacKey = process.env.HMAC_KEY!;
return crypto
.createHmac('sha256', hmacKey)
.update(value.toLowerCase())
.digest('hex');
}
/**
* Comparação de strings em tempo constante (prevenir timing attacks)
*/
safeCompare(a: string, b: string): boolean {
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
/**
* Gerar token seguro para reset de senha, verificação de email, etc.
*/
generateSecureToken(size = 32): string {
return crypto.randomBytes(size).toString('hex');
}
/**
* Mascarar dados sensíveis para logs
*/
maskSensitive(value: string, visible = 4): string {
if (value.length <= visible) return '*'.repeat(value.length);
return value.slice(0, visible) + '*'.repeat(value.length - visible);
}
}
export const cryptoService = new CryptoService();
10. Autorização: RBAC e ABAC {#autorizacao}
// src/shared/authorization/rbac.service.ts
// Definição de permissões
type Resource = 'users' | 'products' | 'orders' | 'admin';
type Action = 'create' | 'read' | 'update' | 'delete' | 'list';
type Permission = `${Resource}:${Action}`;
const ROLE_PERMISSIONS: Record = {
USER: [
'products:read',
'products:list',
'orders:create',
'orders:read',
'orders:list',
'users:read', // Apenas seu próprio perfil (verificar no service)
'users:update', // Apenas seu próprio perfil
],
MODERATOR: [
// Herda USER + extras
'products:read',
'products:list',
'products:update',
'orders:list',
'orders:read',
'orders:update',
'users:list',
'users:read',
'users:update',
],
ADMIN: [
// Todas as permissões
'users:create', 'users:read', 'users:update', 'users:delete', 'users:list',
'products:create', 'products:read', 'products:update', 'products:delete', 'products:list',
'orders:create', 'orders:read', 'orders:update', 'orders:delete', 'orders:list',
'admin:read', 'admin:update',
],
};
export class RBACService {
hasPermission(role: string, permission: Permission): boolean {
const permissions = ROLE_PERMISSIONS[role] || [];
return permissions.includes(permission);
}
requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ message: 'Não autenticado' });
}
if (!this.hasPermission(req.user.role, permission)) {
return res.status(403).json({
message: `Sem permissão: ${permission}`,
});
}
next();
};
}
}
export const rbac = new RBACService();
// ABAC: Attribute-Based Access Control (mais flexível)
interface ABACContext {
user: { id: string; role: string; departmentId?: string };
resource: { ownerId?: string; departmentId?: string; status?: string; [key: string]: any };
action: string;
environment: { ip: string; timestamp: Date };
}
type ABACPolicy = (context: ABACContext) => boolean;
const POLICIES: Record = {
'order:update': [
// Política 1: Dono pode atualizar pedidos pendentes
({ user, resource }) =>
resource.ownerId === user.id && resource.status === 'PENDING',
// Política 2: Moderadores podem atualizar qualquer pedido confirmado
({ user, resource }) =>
user.role === 'MODERATOR' && ['CONFIRMED', 'SHIPPED'].includes(resource.status),
// Política 3: Admins podem tudo
({ user }) => user.role === 'ADMIN',
],
};
export class ABACService {
isAllowed(context: ABACContext): boolean {
const policies = POLICIES[`${context.resource.type}:${context.action}`] || [];
return policies.some(policy => {
try {
return policy(context);
} catch {
return false;
}
});
}
}
11. Auditoria, Logging e Detecção de Intrusão {#auditoria}
// src/shared/audit/audit.service.ts
interface AuditEvent {
userId?: string;
action: string;
resource: string;
resourceId?: string;
ipAddress: string;
userAgent?: string;
success: boolean;
metadata?: Record;
timestamp?: Date;
}
export class AuditService {
async log(event: AuditEvent): Promise {
await auditRepository.create({
...event,
timestamp: event.timestamp || new Date(),
});
// Detectar padrões suspeitos em tempo real
await this.detectSuspiciousActivity(event);
}
private async detectSuspiciousActivity(event: AuditEvent): Promise {
// 1. Múltiplas falhas de login
if (event.action === 'LOGIN' && !event.success) {
const failureKey = `security:login_failures:${event.ipAddress}`;
const failures = await redis.incr(failureKey);
await redis.expire(failureKey, 3600);
if (failures >= 10) {
await this.raiseAlert('MULTIPLE_LOGIN_FAILURES', {
ipAddress: event.ipAddress,
failures,
userId: event.userId,
});
}
}
// 2. Acesso de geolocalização incomum
if (event.action === 'LOGIN' && event.success && event.userId) {
const lastIP = await redis.get(`user:last_ip:${event.userId}`);
if (lastIP && lastIP !== event.ipAddress) {
const currentGeo = await geoIPService.lookup(event.ipAddress);
const lastGeo = await geoIPService.lookup(lastIP);
if (currentGeo.country !== lastGeo.country) {
await this.raiseAlert('LOGIN_FROM_NEW_COUNTRY', {
userId: event.userId,
newCountry: currentGeo.country,
lastCountry: lastGeo.country,
ipAddress: event.ipAddress,
});
}
}
await redis.setex(`user:last_ip:${event.userId}`, 86400, event.ipAddress);
}
// 3. Volume anormal de operações
if (event.userId) {
const volumeKey = `security:ops:${event.userId}:${Math.floor(Date.now() / 60000)}`;
const ops = await redis.incr(volumeKey);
await redis.expire(volumeKey, 120);
if (ops > 1000) { // > 1000 operações por minuto
await this.raiseAlert('HIGH_OPERATION_VOLUME', {
userId: event.userId,
operationsPerMinute: ops,
});
}
}
// 4. Acesso a dados de outros usuários (BOLA attempt)
if (event.action.startsWith('READ_') && !event.success && event.userId) {
const bolaKey = `security:bola:${event.userId}`;
const attempts = await redis.incr(bolaKey);
await redis.expire(bolaKey, 3600);
if (attempts >= 20) {
await this.raiseAlert('POSSIBLE_BOLA_ATTACK', {
userId: event.userId,
attempts,
});
}
}
}
private async raiseAlert(type: string, data: Record): Promise {
console.error(`🚨 SECURITY ALERT: ${type}`, data);
// Notificar equipe de segurança (Slack, PagerDuty, etc.)
await notificationService.sendSecurityAlert({
type,
data,
timestamp: new Date().toISOString(),
});
// Armazenar para análise posterior
await redis.lpush('security:alerts', JSON.stringify({ type, data, timestamp: new Date() }));
await redis.ltrim('security:alerts', 0, 999); // Manter últimos 1000 alertas
}
}
// Middleware de auditoria automática
export function auditMiddleware(action: string, resource: string) {
return async (req: Request, res: Response, next: NextFunction) => {
const originalSend = res.json.bind(res);
let responseStatus: number;
res.json = function(body) {
responseStatus = res.statusCode;
return originalSend(body);
};
next();
// Logar após a resposta
await auditService.log({
userId: req.user?.userId,
action,
resource,
resourceId: req.params.id,
ipAddress: req.ip || 'unknown',
userAgent: req.headers['user-agent'],
success: responseStatus! < 400,
metadata: {
method: req.method,
path: req.path,
statusCode: responseStatus!,
},
});
};
}
12. Security Testing {#testing}
Testes de Segurança Automatizados
// tests/security/auth.security.test.ts
describe('Security Tests - Authentication', () => {
describe('SQL/NoSQL Injection Prevention', () => {
const injectionPayloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
'{"$gt": ""}',
'{"$where": "1==1"}',
'',
'../../../etc/passwd',
'{{7*7}}', // Template injection
];
injectionPayloads.forEach(payload => {
it(`deve rejeitar payload de injeção: ${payload.substring(0, 30)}`, async () => {
const response = await request(app)
.post('/api/v1/auth/login')
.send({ email: payload, password: 'test' });
expect(response.status).toBe(400);
expect(response.body.status).toBe('error');
});
});
});
describe('Rate Limiting', () => {
it('deve bloquear após 10 tentativas de login falhas', async () => {
for (let i = 0; i < 10; i++) {
await request(app)
.post('/api/v1/auth/login')
.send({ email: 'test@example.com', password: 'wrong' });
}
const response = await request(app)
.post('/api/v1/auth/login')
.send({ email: 'test@example.com', password: 'correct' });
expect(response.status).toBe(429);
});
});
describe('BOLA (Broken Object Level Authorization)', () => {
it('não deve permitir acesso a recurso de outro usuário', async () => {
const { accessToken: token1 } = await loginUser('user1@test.com');
const { accessToken: token2, userId: userId2 } = await loginUser('user2@test.com');
// Criar recurso para user2
const order = await createOrder(token2);
// Tentar acessar com token de user1
const response = await request(app)
.get(`/api/v1/orders/${order.id}`)
.set('Authorization', `Bearer ${token1}`);
expect(response.status).toBe(404); // Não revela que existe
});
});
describe('Mass Assignment', () => {
it('não deve permitir escalar privilégios via mass assignment', async () => {
const { accessToken } = await loginUser('user@test.com');
const response = await request(app)
.put('/api/v1/users/me')
.set('Authorization', `Bearer ${accessToken}`)
.send({
name: 'Normal Update',
role: 'ADMIN', // Tentativa de escalar role
isVerified: true, // Tentativa de auto-verificar
createdAt: '2000-01-01', // Tentativa de alterar timestamp
});
expect(response.status).toBe(200);
expect(response.body.data.role).toBe('USER'); // Role não mudou!
});
});
describe('Security Headers', () => {
it('deve incluir todos os headers de segurança obrigatórios', async () => {
const response = await request(app).get('/health');
expect(response.headers['x-content-type-options']).toBe('nosniff');
expect(response.headers['x-frame-options']).toBe('DENY');
expect(response.headers['strict-transport-security']).toBeDefined();
expect(response.headers['content-security-policy']).toBeDefined();
expect(response.headers['x-powered-by']).toBeUndefined(); // Ocultar Express
});
});
describe('Token Security', () => {
it('deve rejeitar token com algoritmo none', async () => {
// Forjar token com alg: none
const payload = btoa(JSON.stringify({ alg: 'none', typ: 'JWT' }));
const data = btoa(JSON.stringify({ userId: 'admin-id', role: 'ADMIN' }));
const fakeToken = `${payload}.${data}.`;
const response = await request(app)
.get('/api/v1/users/me')
.set('Authorization', `Bearer ${fakeToken}`);
expect(response.status).toBe(401);
});
it('deve rejeitar token expirado', async () => {
const expiredToken = jwt.sign(
{ userId: 'user-id', role: 'USER' },
process.env.JWT_SECRET!,
{ expiresIn: '-1s' } // Já expirado
);
const response = await request(app)
.get('/api/v1/users/me')
.set('Authorization', `Bearer ${expiredToken}`);
expect(response.status).toBe(401);
expect(response.body.message).toContain('expirado');
});
});
});
13. Conclusão: Security by Design {#conclusao}
Segurança é uma jornada contínua, não um destino. As ameaças evoluem constantemente, e um sistema que era seguro hoje pode ter vulnerabilidades descobertas amanhã.
Checklist de segurança para todo deployment:
Autenticação e Autorização:
- Senhas hasheadas com bcrypt (salt rounds ≥ 12)
- JWTs com expiração curta + refresh token rotation
- BOLA check em todos os endpoints de recursos
- Whitelist de campos para atualização (evitar mass assignment)
- Rate limiting em todos os endpoints de autenticação
- Bloqueio temporário após N falhas de login
- MFA disponível para usuários sensíveis
Proteção de Dados:
- Validação de entrada com Zod em todos os endpoints
- Queries parametrizadas (ORM previne SQL injection)
- Sanitização de dados antes de armazenar HTML
- Dados sensíveis criptografados em repouso (AES-256-GCM)
- HTTPS obrigatório em produção (HSTS preload)
Headers e Configuração:
- Helmet configurado com CSP, HSTS, frameguard
- CORS restrito a origens confiáveis
- Secrets em variáveis de ambiente (nunca no código)
- Dependências atualizadas (
npm auditno CI/CD) - Imagens Docker com usuário não-root
Monitoramento:
- Logging de todas as operações sensíveis (login, delete, admin)
- Alertas para padrões suspeitos (BOLA attempts, brute force)
- Revisão periódica de logs de auditoria
Ferramentas Recomendadas:
npm audit— Vulnerabilidades em dependências- Snyk — Security scanning contínuo
- OWASP ZAP — DAST (Dynamic Application Security Testing)
- Semgrep — SAST (Static Application Security Testing)
- Trivy — Vulnerabilidades em imagens Docker
- GitGuardian — Prevenção de secrets em repositórios
Recursos para continuar:
- OWASP Testing Guide — owasp.org/www-project-web-security-testing-guide
- PortSwigger Web Security Academy — portswigger.net/web-security (gratuito e excelente)
- HackTheBox e TryHackMe — Laboratórios práticos de segurança
- CWE/SANS Top 25 — Most Dangerous Software Weaknesses
Segurança é responsabilidade de toda a equipe. Cada desenvolvedor deve pensar como um atacante ao escrever código — pergunte sempre: "Se eu quisesse abusar desta API, como faria?"
Publicado em 2025 | Categoria: Segurança Backend | Tags: OWASP, JWT, OAuth2, Node.js Security, XSS, CSRF, SQL Injection, Criptografia, Rate Limiting, MFA