CSRF em SPAs e APIs: cookies, CORS e tokens sem confusão

Fluxo entre uma página maliciosa, cookies, um escudo de proteção, servidor e aplicação web

“Minha API usa JSON, então não sofre CSRF.” Essa frase só é verdadeira com várias condições escondidas. O ataque não depende de formulário HTML; depende de o navegador enviar automaticamente uma credencial que autoriza uma ação. Se a autenticação usa cookie de sessão, uma página maliciosa pode tentar fazer o browser da vítima chamar sua API já autenticado.

Se o cliente envia um bearer token apenas no header Authorization, guardado fora de cookies e incluído explicitamente pelo JavaScript, o CSRF clássico perde seu mecanismo. Isso não torna a aplicação segura contra XSS nem contra vazamento do token. São ameaças diferentes, com defesas diferentes.

Comece pelo modelo de autenticação

  • Sessão em cookie: proteja operações que mudam estado contra CSRF.
  • Token em Authorization: o navegador não o anexa sozinho; o risco principal muda para roubo e exposição do token.
  • Cookie que contém JWT: continua sendo cookie automático. O formato interno não elimina CSRF.

Também não use GET para excluir, pagar, convidar ou alterar dados. Métodos seguros precisam permanecer sem efeito colateral justamente porque navegadores, robôs, caches e prefetchers podem acioná-los.

SameSite ajuda, mas não encerra a conversa

SameSite=Strict oferece a barreira mais forte: o cookie não acompanha requests cross-site. Lax é mais compatível com navegações normais e bloqueia boa parte dos envios cross-site que mudam estado. None permite uso cross-site e exige Secure.

O detalhe perigoso é que site não significa origin. Subdomínios diferentes podem ser same-site. Se um subdomínio estiver comprometido, uma política de cookie ampla ou CORS permissivo pode abrir uma rota que parecia bloqueada. Por isso a OWASP trata SameSite como defesa em profundidade, não como substituto universal de tokens.

O padrão cookie-to-header nas SPAs

Um desenho comum mantém a sessão em cookie HttpOnly e entrega um token CSRF separado, legível pelo JavaScript. A SPA lê esse token e o devolve em um header customizado, como X-CSRF-Token. O servidor aceita a operação apenas quando sessão e token correspondem.

const csrfToken = readCookie('XSRF-TOKEN');

await fetch('/api/profile', {
  method: 'PATCH',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,
  },
  body: JSON.stringify({ displayName }),
});

Aplicações com sessão no servidor costumam usar o synchronizer token pattern. Arquiteturas sem estado podem usar double-submit cookie, mas a variante ingênua é vulnerável a injeção de cookie. Prefira token assinado com HMAC e vinculado à sessão.

CORS não é um filtro decorativo

Um header customizado força preflight em requests cross-origin no navegador. Essa defesa só vale se o servidor autorizar origens exatas. Com credenciais, Access-Control-Allow-Origin: * é rejeitado pelo browser, mas refletir qualquer Origin recebido é apenas um asterisco com roupa nova.

Não aceite text/plain, application/x-www-form-urlencoded ou multipart/form-data em endpoints JSON sem necessidade. Esses tipos permitem requests “simples”, que não passam por preflight. Validar Content-Type reduz a superfície, embora não substitua a defesa principal.

Origin e Fetch Metadata fecham mais portas

O backend pode comparar Origin — e, como fallback, Referer — com a origem esperada. Os headers Fetch Metadata adicionam contexto. Para métodos que alteram estado, Sec-Fetch-Site: cross-site é um sinal forte para bloquear a requisição. Faça rollout em modo de observação antes de impor a regra e mantenha fallback para clientes que não enviam esses headers.

A combinação que eu adotaria

  • Cookie de sessão com Secure, HttpOnly e o SameSite mais restrito compatível com o produto.
  • Token CSRF em header para toda operação autenticada que muda estado.
  • Allowlist exata de origins no CORS, especialmente quando há credenciais.
  • Validação de Origin e política de Fetch Metadata como camadas adicionais.
  • Reautenticação ou confirmação explícita para ações de alto impacto.

CSRF em SPA não pede um truque isolado; pede coerência entre cookie, cliente, CORS e backend. E há um último alerta: JavaScript vulnerável pode receber uma URL controlada pelo atacante e enviar a requisição com o token legítimo. Proteção no servidor não autoriza o cliente a construir endpoints a partir de fragmentos ou parâmetros não confiáveis.

Uma matriz curta evita aplicar a defesa errada

  • Cookie enviado automaticamente: existe superfície de CSRF; use token, SameSite, validação de Origin e Fetch Metadata conforme o fluxo.
  • Bearer lido do storage e colocado pelo JavaScript: o navegador não o envia sozinho, então CSRF clássico perde força; XSS passa a ser a ameaça central.
  • Autenticação mista: se qualquer endpoint aceita cookie, ele precisa de proteção mesmo que outros usem Bearer.
  • Webhook servidor a servidor: não é CSRF; autentique assinatura, origem e replay.

O servidor deve validar antes de executar o efeito, não apenas confiar que o frontend anexou o header certo:

if (usaCookie(request) && metodoAlteraEstado(request)) {
  exigirOriginPermitida(request.headers.origin);
  exigirFetchSiteAceitavel(request.headers['sec-fetch-site']);
  exigirTokenCsrfValido(
    request.cookies.csrf,
    request.headers['x-csrf-token'],
  );
}

Sec-Fetch-Site ausente não deve ser aceito ou rejeitado por reflexo: defina fallback para clientes legados e monitore antes de bloquear. Para ações sensíveis, reautenticação ou confirmação do usuário pode ser uma barreira adicional.

CSRF e XSS se cruzam, mas não se substituem. Uma CSP estrita reduz a chance de script injetado executar; o guia de escaping, sanitização e Trusted Types trata a inserção de HTML. Nenhuma dessas camadas elimina a validação de uma requisição autenticada por cookie.

Fontes