Proteção contra CSRF em SPAs modernas

1. Entendendo o CSRF no Contexto de SPAs

Cross-Site Request Forgery (CSRF) é um ataque onde um site malicioso induz o navegador de uma vítima autenticada a executar ações indesejadas em uma aplicação legítima. Em aplicações web tradicionais baseadas em formulários e cookies de sessão, o ataque explora o fato de que o navegador envia automaticamente cookies de autenticação para o domínio de destino.

SPAs modernas mudam esse cenário de forma significativa. Diferentemente de aplicações tradicionais, SPAs geralmente utilizam autenticação via tokens JWT armazenados em localStorage, sessionStorage ou cookies HttpOnly. Quando o token é armazenado em localStorage, o ataque CSRF tradicional perde efeito, pois o navegador não envia automaticamente esse token. No entanto, muitos SPAs ainda utilizam cookies de sessão ou refresh tokens em cookies, mantendo a superfície de ataque.

O Same-Origin Policy (SOP) e CORS não resolvem o problema sozinhos. O SOP impede que um site malicioso leia a resposta de uma requisição cross-origin, mas não impede que a requisição seja enviada. CORS, por sua vez, é um mecanismo controlado pelo servidor que pode ser mal configurado. Um ataque CSRF clássico não precisa ler a resposta — basta executar a ação.

Exemplo real: uma SPA bancária que expõe um endpoint POST /api/transfer aceitando um cookie de sessão. Um site malicioso cria um formulário ou imagem que dispara essa requisição. Se o usuário estiver logado no banco, a transferência é executada sem seu consentimento.

<!-- Página maliciosa -->
<img src="https://banco.com/api/transfer?amount=1000&to=attacker" style="display:none" />

2. Mecanismos Clássicos de Defesa e suas Limitações em SPAs

Tokens CSRF síncronos funcionam bem em aplicações server-rendered: o servidor insere um token único no formulário HTML, e o cliente o envia de volta. Em SPAs, porém, não há formulários HTML sendo gerados pelo servidor a cada requisição. O token precisa ser obtido via API, adicionando complexidade e uma chamada extra.

Double Submit Cookie funciona assim: o servidor envia um token CSRF em um cookie não HttpOnly e espera receber o mesmo valor em um cabeçalho customizado. O atacante não consegue ler o cookie de outro domínio, então não pode enviar o cabeçalho correto. A fragilidade aparece quando há subdomínios vulneráveis a XSS ou quando o cookie não tem o atributo __Host- prefix.

Cookie-to-header é similar, mas o servidor valida se o cabeçalho corresponde ao cookie. A vulnerabilidade principal é que, se o atacante conseguir injetar JavaScript no mesmo domínio (XSS), ele pode ler o cookie e enviar o cabeçalho. Além disso, subdomínios comprometidos podem acessar cookies sem o atributo __Host-.

// Exemplo de Double Submit Cookie vulnerável
// Servidor envia: Set-Cookie: csrf_token=abc123; Path=/; Domain=.banco.com
// Cliente envia: X-CSRF-Token: abc123
// Problema: subdomínio atacante pode definir cookie para .banco.com

O atributo SameSite é uma defesa poderosa e nativa do navegador. Ele controla quando cookies são enviados em requisições cross-site.

  • SameSite=Strict: nunca envia o cookie em requisições iniciadas por outros sites. Ideal para ações críticas, mas quebra funcionalidades como redirecionamentos OAuth e links externos.
  • SameSite=Lax: envia o cookie apenas em navegações top-level via GET (links, redirecionamentos). É o padrão atual dos navegadores. Bloqueia a maioria dos ataques CSRF, mas permite formulários POST de outros sites.
  • SameSite=None: envia o cookie em todas as requisições. Exige Secure e é o menos seguro.

Para SPAs, a configuração recomendada é:

Set-Cookie: session_token=eyJhbGciOiJIUzI1NiIs...; Path=/; Secure; HttpOnly; SameSite=Lax

Para endpoints críticos (transferências, exclusão de conta), considere SameSite=Strict combinado com verificação de método HTTP.

Mitigação para quebras funcionais: se sua SPA usa OAuth, configure o cookie de sessão com SameSite=Lax e use state parameter para proteção adicional. Para links externos que precisam manter sessão, utilize SameSite=None apenas em cookies específicos e com curta duração.

4. Custom Headers e Verificação de Origem

Cabeçalhos customizados são uma defesa eficaz porque requisições cross-site simples (formulários, imagens) não podem defini-los. O navegador envia uma requisição preflight (OPTIONS) antes de requisições com headers customizados, e o servidor pode rejeitá-la.

Implementação no servidor (Express.js):

// Middleware de verificação de cabeçalho customizado
app.use((req, res, next) => {
  if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
    const csrfHeader = req.headers['x-csrf-token'];
    if (!csrfHeader || csrfHeader !== req.session.csrfToken) {
      return res.status(403).json({ error: 'CSRF token inválido' });
    }
  }
  next();
});

Validação do cabeçalho Origin e Referer:

// Verificação de origem no servidor
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const referer = req.headers.referer;

  const allowedOrigins = ['https://meuspa.com', 'https://api.meuspa.com'];

  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({ error: 'Origem não permitida' });
  }

  if (referer && !referer.startsWith('https://meuspa.com')) {
    return res.status(403).json({ error: 'Referer inválido' });
  }

  next();
});

5. Abordagem Moderna: Token CSRF via API com Refresh

A abordagem mais robusta para SPAs é gerar um token CSRF único por sessão, entregue via endpoint protegido, e incluí-lo em cabeçalho de requisição (nunca em cookie).

Fluxo:
1. Usuário faz login, recebe cookie de sessão HttpOnly + SameSite=Lax
2. SPA chama GET /api/csrf-token (protegido pelo cookie de sessão)
3. Servidor gera token aleatório, armazena na sessão, retorna no corpo da resposta
4. SPA armazena o token em memória (variável JavaScript)
5. Toda requisição mutante inclui X-CSRF-Token: <token>
6. Servidor valida o token contra o armazenado na sessão

Estratégias de renovação:
- Por requisição: novo token a cada requisição. Mais seguro, mas aumenta latência.
- Por tempo: token expira a cada 15-30 minutos. Bom equilíbrio.
- Por evento de autenticação: renovar no login e refresh de token.

// Servidor (Node.js/Express)
app.get('/api/csrf-token', (req, res) => {
  const token = crypto.randomBytes(32).toString('hex');
  req.session.csrfToken = token;
  res.json({ csrfToken: token });
});

// Middleware de validação
function csrfProtection(req, res, next) {
  const token = req.headers['x-csrf-token'];
  if (req.method !== 'GET' && (!token || token !== req.session.csrfToken)) {
    return res.status(403).json({ error: 'CSRF token inválido' });
  }
  next();
}

6. Integração com Frameworks de SPA (React, Vue, Angular)

React com Axios:

// Interceptor para anexar token CSRF
import axios from 'axios';

let csrfToken = null;

export async function fetchCsrfToken() {
  const response = await axios.get('/api/csrf-token', { withCredentials: true });
  csrfToken = response.data.csrfToken;
}

axios.interceptors.request.use(config => {
  if (csrfToken && ['post', 'put', 'patch', 'delete'].includes(config.method)) {
    config.headers['X-CSRF-Token'] = csrfToken;
  }
  config.withCredentials = true;
  return config;
});

Vue com Fetch API:

// store/csrf.js
let csrfToken = null;

export async function initializeCsrf() {
  const response = await fetch('/api/csrf-token', { credentials: 'include' });
  const data = await response.json();
  csrfToken = data.csrfToken;
}

export function getCsrfToken() {
  return csrfToken;
}

// Uso em chamadas API
async function transfer(amount, toAccount) {
  const response = await fetch('/api/transfer', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': getCsrfToken()
    },
    body: JSON.stringify({ amount, toAccount })
  });
  return response.json();
}

Boas práticas de armazenamento: armazene o token CSRF em memória (variável no escopo do módulo ou contexto React). Nunca em localStorage ou sessionStorage, pois XSS pode ler. Nunca em cookie não HttpOnly.

7. Testando e Validando a Proteção

Simulação manual de ataque CSRF:

<!-- Teste: crie um HTML externo -->
<html>
<body>
  <form action="https://seuspa.com/api/transfer" method="POST">
    <input type="hidden" name="amount" value="1000">
    <input type="hidden" name="to" value="attacker">
  </form>
  <script>document.forms[0].submit();</script>
</body>
</html>

Abra este HTML em outro domínio enquanto está logado na SPA. Se a transferência for executada, a proteção falhou.

Testes automatizados com Cypress:

describe('Proteção CSRF', () => {
  it('deve bloquear requisição sem token CSRF', () => {
    cy.request({
      method: 'POST',
      url: '/api/transfer',
      body: { amount: 100, to: 'test' },
      failOnStatusCode: false
    }).then((response) => {
      expect(response.status).to.eq(403);
    });
  });

  it('deve aceitar requisição com token CSRF válido', () => {
    cy.request('/api/csrf-token').then((csrfResponse) => {
      const token = csrfResponse.body.csrfToken;
      cy.request({
        method: 'POST',
        url: '/api/transfer',
        headers: { 'X-CSRF-Token': token },
        body: { amount: 100, to: 'test' }
      }).then((response) => {
        expect(response.status).to.eq(200);
      });
    });
  });
});

Checklist de verificação:
- [ ] Cookies de sessão têm SameSite=Lax ou Strict
- [ ] Cookies críticos têm HttpOnly e Secure
- [ ] Endpoints mutantes exigem cabeçalho customizado
- [ ] Token CSRF armazenado apenas em memória no cliente
- [ ] Servidor valida Origin e/ou Referer
- [ ] Token CSRF é renovado periodicamente
- [ ] Testes automatizados simulam ataques cross-site

8. Considerações Finais e Boas Práticas

A proteção contra CSRF em SPAs modernas exige uma abordagem em camadas (defense in depth). Nenhuma defesa isolada é suficiente. A combinação recomendada é:

  1. SameSite=Lax em cookies de sessão
  2. Token CSRF via API armazenado em memória
  3. Verificação de cabeçalho Origin no servidor
  4. Cabeçalho customizado em requisições mutantes

É crucial não confundir proteção CSRF com prevenção de XSS. CSRF protege contra ações não intencionais iniciadas por outros sites. XSS protege contra injeção de scripts no mesmo site. Um site vulnerável a XSS anula qualquer proteção CSRF, pois o atacante pode ler tokens e cookies diretamente.

Para equipes de desenvolvimento: documente claramente o fluxo de proteção CSRF na sua SPA. Realize revisões de código focadas em segurança. Monitore logs de rejeição CSRF (status 403) para detectar possíveis ataques ou bugs na implementação.

// Exemplo de log de rejeição CSRF
{
  "timestamp": "2025-01-15T10:30:00Z",
  "ip": "192.168.1.100",
  "method": "POST",
  "path": "/api/transfer",
  "reason": "CSRF token ausente",
  "origin": "https://site-malicioso.com",
  "userAgent": "Mozilla/5.0..."
}

Referências