⚡ Últimas

Segurança em APIs Backend Node.js: Guia Completo 2025 — OWASP Top 10, JWT, OAuth2, Criptografia e Proteção Avançada contra Ataques

Sumário

  1. Por que Segurança Backend é uma Habilidade Obrigatória
  2. OWASP API Security Top 10 — 2023
  3. Autenticação Robusta: JWT Best Practices
  4. OAuth2 e OpenID Connect: Autenticação de Terceiros
  5. Multi-Factor Authentication (MFA/TOTP)
  6. Proteção contra Injeção: SQL, NoSQL e Command Injection
  7. Cross-Site Scripting (XSS) e Content Security Policy
  8. CSRF: Cross-Site Request Forgery
  9. Rate Limiting e Proteção contra DDoS
  10. Criptografia: Hashing, Encryption e Secrets Management
  11. HTTPS, TLS e Certificados
  12. Headers de Segurança HTTP
  13. Autorização: RBAC e ABAC
  14. Auditoria, Logging e Detecção de Intrusão
  15. Secrets Management e Configuração Segura
  16. Security Testing: SAST, DAST e Penetration Testing
  17. 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 audit no 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

🌐 Idioma
🇧🇷 Português
🇺🇸 English
🇪🇸 Español
🇫🇷 Français
🇩🇪 Deutsch
💳Faça um Orçamento