Destino Dev

Blog

CSRF em SPAs: SameSite, cookie de duplo envio e tokens — passo a passo

Por John Pires · 11 de março de 2026

Contexto: quando você vai usar isso

O problema em uma frase

Cross-Site Request Forgery (CSRF) explora a confiança que um site A tem no navegador do usuário que está autenticado em B. Em SPAs (Single Page Applications) a autenticação com cookies continua comum: o navegador guarda um cookie de sessão e envia automaticamente em requisições. Se um atacante induzir o navegador do usuário a fazer uma requisição válida para B, a ação será executada com as credenciais do usuário. Você precisa de defesas sempre que usar cookies de autenticação que são enviados automaticamente.

Quando aplicar as estratégias descritas aqui

Use essas técnicas quando a sua SPA autentica usuários com cookies (HttpOnly ou não) ou quando mistura cookies com tokens. Se seu SPA usar somente Authorization: Bearer com armazenamento em memória ou localStorage e enviar o token manualmente, CSRF é menos crítico, mas outros riscos (XSS) aumentam. Neste artigo cobrimos três abordagens úteis: SameSite, cookie de duplo envio e tokens anti-CSRF (síncrono com backend).

As explicações e exemplos abaixo assumem um fluxo comum: backend em Node.js/Express e frontend em JavaScript (vanilla). As técnicas aplicam a outras stacks, mas os exemplos são executáveis e simples de replicar.

Explicação visual com diagramas mentais

Analogia: porta com chave, mas janela aberta

Pense no cookie de sessão como uma chave que o navegador automaticamente leva para a porta do site. CSRF é quando um visitante malicioso abre uma janela (um site atacador) e manda o seu navegador bater na porta do site autenticado usando a chave que o usuário já tinha. SameSite é como fechar a janela automaticamente quando a requisição for de outro site. O cookie de duplo envio é como exigir que, além da chave, o visitante apresente um bilhete secreto que só o dono da chave tem — o bilhete não é enviado automaticamente.

Fluxos em diagramas mentais (texto)

  • Fluxo vulnerável: usuário autenticado em victim.com; atacante faz o navegador enviar POST para victim.com/change-email; cookie de sessão vai automaticamente e ação é executada.
  • SameSite=Lax/Strict: navegador bloqueia envio do cookie em requisições cross-site (dependendo do tipo), impedindo execução automática.
  • Double-submit cookie: servidor exige que o token enviado no header ou body corresponda ao cookie; um site externo não consegue ler o cookie HttpOnly para obter o token, então falha.

Esses fluxos mostram onde cada proteção atua: SameSite atua no próprio navegador; double-submit e tokens precisam de coordenação entre frontend e backend.

Primeiro exemplo simples

Objetivo do exemplo

Mostrar em passos mínimos: 1) configurar cookie com SameSite; 2) fazer um endpoint que exige token de CSRF no header; 3) frontend que lê cookie não HttpOnly e faz requisições com token. Ideal para entender o mecanismo básico.

Backend: Express configurando cookie e endpoint que verifica token

Contexto antes do código: o trecho abaixo cria um servidor Express que emite um cookie de sessão (simulado) e um cookie csrfToken (não HttpOnly) e verifica o header X-CSRF-Token nas requisições sensíveis.

// server-simple.js
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.json());
app.use(cookieParser());

// Simula login: define cookie de sessão e csrf token
app.post('/login', (req, res) => {
  // Em produção, crie session id seguro
  const sessionId = 'sess-12345';
  const csrfToken = 'csrf-' + Math.random().toString(36).slice(2);
  // cookie de sessão (HttpOnly) e cookie csrf (não HttpOnly para leitura pelo JS)
  res.cookie('session', sessionId, { httpOnly: true, sameSite: 'lax', secure: false });
  res.cookie('csrfToken', csrfToken, { httpOnly: false, sameSite: 'lax', secure: false });
  res.json({ ok: true });
});

// Endpoint sensível que exige header X-CSRF-Token
app.post('/api/change-email', (req, res) => {
  const headerToken = req.get('x-csrf-token');
  const cookieToken = req.cookies['csrfToken'];
  if (!headerToken || !cookieToken || headerToken !== cookieToken) {
    return res.status(403).json({ error: 'csrf check failed' });
  }
  // ação sensível simulada
  res.json({ ok: true, email: req.body.email });
});

app.listen(3000);
console.log('server running on http://localhost:3000');

O que acontece depois: o servidor define cookies; o frontend lê o cookie csrfToken e o envia em header X-CSRF-Token. Se o token não bater com o cookie (ou faltar), o servidor retorna 403.

Frontend: SPA mínimo que usa o cookie csrfToken

Contexto: frontend roda em outro domínio ou mesmo host. Exemplo usando fetch para demonstrar o duplo envio do token.

// frontend-simple.js (executar no console ou arquivo servido por servidor estático)
// Função utilitária para ler cookie por nome
function readCookie(name) {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? match[2] : null;
}

async function changeEmail(newEmail) {
  const csrf = readCookie('csrfToken');
  const res = await fetch('http://localhost:3000/api/change-email', {
    method: 'POST',
    credentials: 'include', // envia cookies
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrf
    },
    body: JSON.stringify({ email: newEmail })
  });
  return res.json();
}

// uso: changeEmail('novo@exemplo.com').then(console.log).catch(console.error);

O que acontece depois: o fetch envia automaticamente o cookie de sessão (credentials: 'include') e inclui no header o csrfToken lido do cookie. O servidor valida e aceita a ação.

Segundo exemplo mais realista

Objetivo do exemplo realista

Construir fluxo completo que mostra: login, proteção SameSite, comportamento com cross-site, e validação servidor-side. Além disso, mostrar variação onde o csrf cookie é HttpOnly e o token é retornado via endpoint seguro (double-submit vs token-assignment).

Backend: double-submit cookie com verificação baseada em cookie + header

Contexto: aqui o cookie csrfToken é colocado sem HttpOnly para a SPA ler. Em alternativa mostramos método onde o token é colocado em resposta JSON (quando cookie é HttpOnly) e guardado em memória local pela SPA.

// server-realistic.js
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.json());
app.use(cookieParser());

// rota de login que define session e csrf cookies
app.post('/login', (req, res) => {
  const sessionId = 'sess-' + Math.random().toString(36).slice(2);
  const csrfToken = 'csrf-' + Math.random().toString(36).slice(2);
  // session HttpOnly; csrf não HttpOnly para leitura pelo JS
  res.cookie('session', sessionId, { httpOnly: true, secure: false, sameSite: 'lax' });
  res.cookie('csrfToken', csrfToken, { httpOnly: false, secure: false, sameSite: 'lax' });
  res.json({ ok: true });
});

// opção: rota que retorna token via JSON quando cookie csrfToken é HttpOnly
app.get('/session-info', (req, res) => {
  // suponha que sessao é válida
  const csrfToken = 'csrf-' + Math.random().toString(36).slice(2);
  // se não queremos expor cookie, retornamos por JSON
  res.json({ csrfToken });
});

// Endpoint que exige token no header, compara com cookie
app.post('/api/transfer', (req, res) => {
  const headerToken = req.get('x-csrf-token');
  const cookieToken = req.cookies['csrfToken'];
  if (!headerToken || !cookieToken || headerToken !== cookieToken) {
    return res.status(403).json({ error: 'csrf mismatch' });
  }
  // processo de transferência simulado
  res.json({ ok: true, amount: req.body.amount });
});

app.listen(4000);
console.log('server realistic on http://localhost:4000');

O que acontece depois: esse servidor permite duas variantes. Se o csrfToken estiver como cookie não HttpOnly, o frontend lê e envia no header. Se estiver HttpOnly, o frontend pede /session-info para receber token via JSON (desaconselhável sem autenticação adequada, mas possível).

Frontend realista: tratamento de SameSite e pedido de token

Contexto: exemplos a seguir ilustram duas abordagens no frontend — ler cookie ou buscar token via API. Incluímos tratamento de credentials e fallback.

// frontend-realistic.js
// Ler cookie (quando não HttpOnly)
function readCookie(name) {
  const m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return m ? m[2] : null;
}

// Lógica que tenta ler cookie, se falhar busca /session-info
async function getCsrfToken() {
  const cookieToken = readCookie('csrfToken');
  if (cookieToken) return cookieToken;
  // fallback: buscar via endpoint
  const res = await fetch('http://localhost:4000/session-info', { credentials: 'include' });
  const json = await res.json();
  return json.csrfToken;
}

async function transfer(amount) {
  const token = await getCsrfToken();
  const res = await fetch('http://localhost:4000/api/transfer', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token },
    body: JSON.stringify({ amount })
  });
  return res.json();
}

// uso: transfer(100).then(console.log)

O que acontece depois: o frontend cuida do token; em requisições cross-site o SameSite ajuda a bloquear envio do cookie de sessão. Se o cookie de sessão for enviado mesmo assim (ex: SameSite=None e secure configurado), o servidor continuará exigindo token que sites de terceiros não conseguem gerar corretamente.

Erros comuns de iniciante e como evitar

Erro 1: confiar apenas em SameSite

SameSite é eficaz, mas tem limites: versões antigas de navegadores têm comportamento inconsistente e alguns fluxos legítimos (OAuth redirects, cross-site iframe intencional) exigem SameSite=None. Nunca dependa apenas de SameSite; combine com verificação de token quando a sensibilidade da ação justificar.

Erro 2: guardar token CSRF em localStorage

Colocar token CSRF em localStorage e usá-lo é funcional, mas localStorage é vulnerável a XSS. Se um atacante tem XSS, ele pode extrair tokens. A prática mais segura é manter cookie HttpOnly para sessão e emitir token anti-CSRF em cookie não HttpOnly com duração curta, ou usar SameSite estrito para reduzir superfície de ataque.

Erro 3: tornar o cookie csrf HttpOnly e esperar que frontend leia

Se você marcar csrfToken como HttpOnly esperando que o frontend leia, vai falhar — o JS não consegue ler HttpOnly. Nesse caso, forneça token via endpoint seguro após autenticação (e ainda valide no servidor). Mas lembre-se: uma rota que retorna o token deve exigir autenticação segura para evitar exposição.

Erro 4: comparar tokens apenas por existência

Verifique igualdade exata entre token do cookie e token recebido (double-submit), e faça verificações adicionais (timestamp, vinculação à sessão). Tokens previsíveis ou sem expiração reduzem a eficácia — gere com entropia criptográfica e renove periodicamente.

Dica: use bibliotecas maduras (csurf para Express, middleware das frameworks) quando possível, mas entenda o que elas fazem: gerar token, colocar em cookie e verificar comparação.

Conclusão prática

Resumo das abordagens e quando usar

SameSite é a primeira linha de defesa: configure cookies com SameSite=Lax ou Strict para operações padrão. Para ações sensíveis (mudança de senha, transferências) combine SameSite com um token anti-CSRF validado server-side. O padrão double-submit cookie (cookie não HttpOnly + header) é simples e eficiente em SPAs. Se preferir cookies HttpOnly para sessão e não expor token em cookie, forneça token via endpoint autenticado.

Padrão recomendado para SPAs iniciantes

  1. Configure cookie de sessão: HttpOnly, Secure (em produção), SameSite=Lax (ou Strict se possível).
  2. Implemente token anti-CSRF: double-submit cookie ou endpoint que fornece token após autenticação.
  3. Valide sempre no servidor: compare header com cookie (ou token com sessão).
  4. Evite armazenar tokens em localStorage quando possível; minimize exposição a XSS.

Combinando essas medidas você protege a maior parte dos vetores CSRF sem complicar o fluxo do usuário. Teste sempre em diferentes navegadores e com ferramentas de proxy para simular requisições cross-site.

Checklist prático

  • Cookies de sessão: HttpOnly + Secure + SameSite configurado.
  • Endpoints sensíveis exigem X-CSRF-Token ou validação de origem.
  • Tokens gerados com entropia criptográfica e renovados.
  • Use credentials: 'include' no fetch quando necessário e confirme SameSite durante testes.

Implementando os exemplos deste artigo você terá uma base robusta para proteger SPAs contra CSRF, entenderá qual mecanismo atua em cada etapa e saberá quais armadilhas evitar.

Assuntos: csrf, samesite, cookies, spa, Segurança

Leia também