Gerenciamento de sessão seguro

1. Fundamentos de Sessões Web Seguras

Uma sessão web representa o estado de interação entre um cliente e um servidor durante um período de atividade contínua. Quando um usuário faz login, o servidor cria uma sessão para manter o contexto autenticado sem exigir que as credenciais sejam reenviadas a cada requisição. A segurança desse mecanismo é crítica porque uma sessão comprometida equivale a um acesso não autorizado completo.

A principal diferença entre sessão no servidor e token no cliente está no armazenamento do estado. Em sessões tradicionais, o servidor mantém os dados da sessão (em memória, banco ou cache) e envia apenas um identificador ao cliente. Já em abordagens stateless como JWT, o token contém os próprios dados da sessão, assinados criptograficamente. Cada modelo tem implicações de segurança distintas.

O ciclo de vida seguro de uma sessão compreende três fases:
- Criação: geração de um identificador imprevisível e associação ao usuário autenticado
- Manutenção: verificação contínua da validade e integridade da sessão
- Destruição: invalidação explícita no servidor, não apenas no cliente

2. Geração e Armazenamento Seguro de Identificadores

Gerar session IDs previsíveis é uma das falhas mais exploradas. Use geradores criptograficamente seguros, como o módulo crypto do Node.js:

const crypto = require('crypto');
const sessionId = crypto.randomBytes(32).toString('hex');

O tamanho mínimo recomendado é de 128 bits (16 bytes), mas 256 bits (32 bytes) oferece margem confortável contra ataques de força bruta. A entropia deve vir de uma fonte segura do sistema operacional, não de funções como Math.random().

O armazenamento do session ID deve usar cookies com atributos de segurança obrigatórios:

Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
  • HttpOnly: impede acesso via JavaScript, bloqueando ataques XSS
  • Secure: exige conexão HTTPS, prevenindo interceptação em rede
  • SameSite=Strict: restringe envio do cookie a requisições originadas do mesmo site
  • Path=/: limita o escopo do cookie ao domínio específico

3. Proteção contra Sequestro de Sessão (Session Hijacking)

O sequestro de sessão ocorre quando um atacante obtém o session ID de um usuário legítimo. A primeira linha de defesa é regenerar o identificador após eventos críticos:

// Após login bem-sucedido
req.session.regenerate((err) => {
  if (err) return handleError(err);
  req.session.userId = user.id;
  req.session.createdAt = Date.now();
  res.redirect('/dashboard');
});

A validação de fingerprint (combinação de IP e User-Agent) pode adicionar segurança, mas deve ser usada com cautela: IPs podem mudar em redes móveis ou VPNs, criando falsos positivos. Uma abordagem mais robusta é registrar o fingerprint e alertar o usuário sobre mudanças, sem bloquear automaticamente.

Implemente dois tipos de expiração:
- Expiração absoluta: invalida a sessão após um tempo máximo (ex: 24 horas)
- Expiração por inatividade: invalida após período sem atividade (ex: 30 minutos)

// Verificação de inatividade
const maxInactive = 30 * 60 * 1000; // 30 minutos
if (Date.now() - req.session.lastActivity > maxInactive) {
  req.session.destroy();
  return res.status(401).send('Sessão expirada por inatividade');
}
req.session.lastActivity = Date.now();

4. Defesas contra Fixação de Sessão (Session Fixation)

Fixação de sessão ocorre quando um atacante força a vítima a usar um session ID conhecido. A defesa principal é nunca aceitar um session ID fornecido pelo cliente sem autenticação prévia. Isso significa que o servidor deve sempre gerar um novo identificador após o login, independentemente de qual ID foi enviado.

A troca obrigatória do identificador após autenticação bem-sucedida é a medida mais eficaz:

// Após validação de credenciais
if (validCredentials) {
  const newSessionId = crypto.randomBytes(32).toString('hex');
  // Armazena nova sessão com dados do usuário
  await storeSession(newSessionId, { userId: user.id });
  // Invalida sessão antiga
  await deleteSession(oldSessionId);
  // Define novo cookie
  res.cookie('sessionId', newSessionId, cookieOptions);
}

Sempre invalide sessões antigas ao criar uma nova sessão autenticada. Não basta sobrescrever os dados — o identificador anterior deve ser removido do armazenamento do servidor.

5. Cookies Seguros e Configuração de Headers

A configuração fina de cookies previne múltiplos vetores de ataque. Use Expires ou Max-Age para definir tempo de vida, mas prefira Max-Age por ser relativo ao momento da criação:

Set-Cookie: sessionId=xyz789; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

O prefixo __Host- vincula o cookie à origem completa (domínio + porta), impedindo que subdomínios maliciosos o leiam:

Set-Cookie: __Host-sessionId=abc123; Secure; Path=/; HttpOnly; SameSite=Strict

Já o prefixo __Secure- exige que o cookie seja marcado como Secure, mas não impõe restrições de path. Use __Host- sempre que possível para maior segurança.

Headers de segurança adicionais no Set-Cookie incluem:
- Domain: evite definir domínios amplos; omita para restringir ao servidor de origem
- Partitioned: útil em ambientes de terceiros, mas requer análise cuidadosa de privacidade

6. Logout e Encerramento de Sessão

Um erro comum é apenas remover o cookie do lado cliente, deixando a sessão ativa no servidor. O logout seguro deve invalidar a sessão no servidor:

app.post('/logout', (req, res) => {
  const sessionId = req.cookies.sessionId;
  if (sessionId) {
    // Remove do banco ou cache
    sessionStore.destroy(sessionId, (err) => {
      if (err) console.error('Erro ao destruir sessão:', err);
    });
  }
  // Limpa cookie do cliente
  res.clearCookie('sessionId', { path: '/' });
  res.status(200).send('Logout realizado');
});

Para logout em todos os dispositivos (single sign-out), mantenha um registro de todas as sessões ativas de um usuário:

// Ao fazer login, adiciona à lista de sessões do usuário
await db.collection('userSessions').insertOne({
  userId: user.id,
  sessionId: newSessionId,
  createdAt: new Date()
});

// No logout global
await db.collection('userSessions').deleteMany({ userId: user.id });
// Remove cada sessão do cache
userSessions.forEach(s => sessionStore.destroy(s.sessionId));

7. Boas Práticas em APIs e Sessões Stateless

Para APIs RESTful, JWTs são comuns, mas exigem cuidados específicos. Use assinatura forte com HS256 (segredo compartilhado) ou RS256 (par de chaves):

// Geração de JWT com expiração curta
const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { algorithm: 'HS256', expiresIn: '15m' }
);

No lado cliente, armazene tokens em memória (variável JavaScript) sempre que possível. Evite localStorage por ser acessível via XSS. Para SPAs, use cookies HttpOnly com refresh tokens armazenados em memória:

// Refresh token rotativo
app.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.status(401).send('Token ausente');

  const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
  // Gera novo access token e novo refresh token
  const newAccessToken = generateAccessToken(decoded.userId);
  const newRefreshToken = generateRefreshToken(decoded.userId);

  // Invalida refresh token antigo (blacklist)
  blacklistToken(refreshToken);

  res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken });
});

8. Monitoramento e Resposta a Incidentes

Registre eventos de sessão sem expor dados sensíveis:

// Logging seguro
logger.info({
  event: 'login_success',
  userId: user.id,
  ip: req.ip,
  timestamp: new Date().toISOString(),
  sessionId: hashSessionId(req.sessionID) // hash para correlação sem expor ID
});

Detecte anomalias como múltiplos IPs simultâneos para o mesmo usuário:

// Verificação de múltiplos IPs
const activeSessions = await getActiveSessions(userId);
const uniqueIPs = new Set(activeSessions.map(s => s.ip));
if (uniqueIPs.size > 3) {
  alertSecurityTeam(userId, 'Múltiplos IPs detectados');
  invalidateAllSessions(userId);
}

Em caso de vazamento suspeito, implemente invalidação forçada:
- Force logout de todas as sessões do usuário afetado
- Exija reautenticação com verificação adicional (2FA)
- Rotacione chaves de assinatura se JWTs foram comprometidos

Referências