“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,HttpOnlye 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.


