CSRF em aplicações Node.js é um tema importante para quem desenvolve serviços modernos com JavaScript no servidor. Não basta fazer uma demonstração funcionar no computador local: uma solução profissional precisa ter contratos claros, tratamento de falhas, segurança, testes, observabilidade e um processo de implantação previsível.
Neste guia, você aprenderá a aplicar CSRF em aplicações Node.js de forma prática, entendendo a arquitetura, os principais componentes, os riscos mais comuns e as decisões que tornam o projeto mais fácil de manter. O objetivo é sair de um exemplo isolado e chegar a uma base que possa evoluir sem depender de improvisos.
O que é CSRF em aplicações Node.js?
CSRF, ou Cross-Site Request Forgery, ocorre quando um navegador autenticado envia uma requisição de alteração iniciada por um site malicioso. O ataque aproveita principalmente credenciais enviadas automaticamente, como cookies de sessão. Ele não depende de ler a resposta: basta induzir uma ação válida em nome da vítima.
Em aplicações Node.js, essa abordagem se encaixa bem em sistemas orientados a eventos e operações de entrada e saída. Para revisar os fundamentos da plataforma, leia o que é Node.js. Se você ainda está montando seu primeiro backend, o tutorial sobre como criar uma API com Node.js oferece uma visão complementar.
Quando vale a pena usar?
A proteção é necessária em aplicações web que usam cookies, autenticação por sessão, certificados de cliente ou qualquer credencial anexada automaticamente pelo navegador. APIs que recebem tokens exclusivamente no header Authorization têm um perfil diferente, mas ainda devem controlar CORS, XSS e vazamento de tokens.
A decisão deve considerar o problema real, a experiência da equipe, a infraestrutura disponível e o custo de operação. Uma tecnologia pode ser excelente e ainda assim ser inadequada quando aumenta a complexidade sem gerar benefício mensurável. Comece pequeno, defina critérios de sucesso e valide o comportamento sob carga e falhas.
Preparando o projeto
Crie uma pasta dedicada, inicialize o package.json e fixe uma versão suportada do Node.js no arquivo de configuração do projeto. Use variáveis de ambiente para endereços, credenciais e opções de execução, mas valide tudo no início do processo para evitar falhas tardias.
npm init -y
npm install express cookie-parser
npm install --save-dev supertestSepare dependências de produção das ferramentas de desenvolvimento. Ative lint, formatação e testes no pipeline de integração contínua. O artigo sobre deploy com GitHub Actions mostra como automatizar verificações antes que uma alteração chegue ao servidor.
Exemplo inicial
Uma defesa prática combina cookies SameSite, validação de Origin e um token imprevisível associado à sessão. O exemplo abaixo mostra a verificação básica do token e da origem.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.use(express.urlencoded({ extended: false }));
function safeEqual(left, right) {
if (!left || !right) return false;
const a = Buffer.from(left);
const b = Buffer.from(right);
return a.length === b.length &&
crypto.timingSafeEqual(a, b);
}
app.post('/profile/email', (req, res) => {
const origin = req.get('origin');
if (origin !== 'https://app.example.com') {
return res.status(403).json({ code: 'INVALID_ORIGIN' });
}
if (!safeEqual(req.body.csrfToken, req.session.csrfToken)) {
return res.status(403).json({ code: 'INVALID_CSRF_TOKEN' });
}
res.status(204).end();
});O token deve ser criado com alta entropia, armazenado na sessão e inserido no formulário ou enviado em um header customizado. A validação de Origin acrescenta uma camada importante, mas precisa considerar proxies confiáveis e origens exatas.
Estrutura recomendada
Centralize a geração e a validação em um middleware. A camada de sessão deve emitir um token por sessão ou por operação sensível, expirar registros antigos e rejeitar reutilização quando o risco justificar. Rotas somente de leitura não devem alterar estado, pois isso reduz pontos suscetíveis.
- config: leitura e validação de ambiente;
- domain: regras de negócio independentes do transporte;
- application: casos de uso e orquestração;
- infrastructure: banco, filas, rede e integrações;
- interfaces: HTTP, comandos, eventos ou tarefas agendadas;
- tests: unidades, integração e cenários de contrato.
Essa separação reduz acoplamento e permite substituir bibliotecas sem reescrever regras de negócio. Evite criar camadas vazias apenas para seguir um padrão: cada módulo deve proteger uma responsabilidade concreta.
Contratos e validação
Dados externos são sempre não confiáveis. Valide corpo, parâmetros, headers, eventos e respostas de serviços terceiros. Defina tamanho máximo, formatos permitidos e mensagens de erro consistentes. Schemas executáveis aproximam documentação e comportamento real; veja também o guia de validação com Zod no TypeScript.
Valide método HTTP, Content-Type, Origin, Referer como fallback controlado e o token CSRF. Rejeite requisições de alteração sem origem quando o fluxo normal sempre passa por um navegador moderno. Não use comparação simples para segredos e limite o tamanho de headers e campos.
Tratamento de erros
Classifique falhas em categorias: entrada inválida, autenticação, autorização, recurso ausente, conflito, indisponibilidade temporária e erro interno. A resposta pública deve ser estável e não pode expor stack trace, consulta SQL, segredo ou detalhes da infraestrutura.
Respostas de falha devem usar código estável e não revelar o token esperado. Registre rota, método, origem e requestId, mas não registre cookies ou tokens completos. Diferencie falha CSRF de sessão expirada para diagnóstico interno, sem criar mensagens excessivamente detalhadas ao cliente.
function requireCsrf(req, res, next) {
const token = req.get('x-csrf-token') ?? req.body.csrfToken;
const expected = req.session?.csrfToken;
if (!safeEqual(token, expected)) {
req.log?.warn({
event: 'csrf_rejected',
origin: req.get('origin'),
requestId: req.id
});
return res.status(403).json({ code: 'REQUEST_REJECTED' });
}
next();
}Segurança
A segurança deve fazer parte da arquitetura desde o início. Aplique privilégio mínimo, valide todas as fronteiras e trate credenciais como dados sensíveis. Para CSRF em aplicações Node.js, priorize:
- configure cookies com Secure, HttpOnly e SameSite adequado ao fluxo
- não use GET para operações que alteram estado
- valide Origin em requisições sensíveis
- gere tokens com fonte criptográfica segura
- proteja o token contra exposição por XSS
- reavalie exceções para integrações e webhooks
Registre eventos de segurança sem armazenar tokens completos, senhas ou dados pessoais desnecessários. Revise dependências e mantenha um processo claro para corrigir vulnerabilidades.
Desempenho e capacidade
Otimização começa com medição. Defina indicadores como latência, taxa de erro, uso de memória, CPU, tamanho de filas e tempo de dependências. O guia sobre como otimizar APIs RESTful em Node.js detalha princípios que também se aplicam aqui.
- gere o token uma vez por sessão quando o modelo permitir
- evite consultas ao banco em toda validação
- mantenha comparação e parsing de headers simples
- meça rejeições por rota sem labels de alta cardinalidade
- expire sessões e tokens antigos
- não faça rotação excessiva que quebre múltiplas abas
Teste com carga representativa e inclua cenários de falha. Uma solução que funciona apenas com dependências saudáveis não está pronta para produção.
Observabilidade
Acompanhe rejeições por rota, método, resultado da validação de origem e tipo de cliente. Um aumento súbito pode indicar ataque, frontend desatualizado ou configuração incorreta de proxy. Use amostragem nos logs para evitar volume excessivo em ataques automatizados.
Use logs estruturados em JSON e inclua um identificador de correlação. Métricas devem mostrar volume, sucesso, falhas e duração. Traces distribuídos ajudam a encontrar gargalos quando uma operação atravessa vários serviços, mas precisam de amostragem para controlar custo.
Testes automatizados
Teste token válido, ausente, incorreto, origem divergente, sessão expirada e método inesperado. Confirme que nenhuma operação de estado acontece antes da validação. Inclua cenários com múltiplas abas e renovação de sessão.
import test from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import { app, sessionForTest } from './app.js';
test('rejeita token inválido', async () => {
const cookie = await sessionForTest({ csrfToken: 'expected' });
const response = await request(app)
.post('/profile/email')
.set('Cookie', cookie)
.set('Origin', 'https://app.example.com')
.send({ csrfToken: 'wrong' });
assert.equal(response.status, 403);
});
test('aceita token e origem válidos', async () => {
const cookie = await sessionForTest({ csrfToken: 'expected' });
const response = await request(app)
.post('/profile/email')
.set('Cookie', cookie)
.set('Origin', 'https://app.example.com')
.send({ csrfToken: 'expected' });
assert.equal(response.status, 204);
});Evite testes dependentes de ordem, relógio real ou serviços externos instáveis. Injete relógio, geradores de identificadores e clientes de infraestrutura. Para fundamentos, consulte testes unitários com Jest.
Implantação e operação
Confirme a configuração de HTTPS e trust proxy antes de depender de cookies Secure ou do protocolo observado pela aplicação. Documente origens permitidas por ambiente e não mantenha domínios de desenvolvimento na produção. Faça rollout monitorado para identificar clientes legítimos bloqueados.
Implemente graceful shutdown: ao receber um sinal de encerramento, pare de aceitar trabalho novo, conclua o que estiver em andamento dentro de um prazo e feche conexões. Configure health checks que diferenciem processo vivo de serviço pronto para receber tráfego.
Erros comuns
- Confiar somente em SameSite: o atributo ajuda, mas não cobre todos os fluxos e navegadores
- Usar GET para alterações: links e prefetch podem disparar ações inesperadas
- Aceitar qualquer Origin: a validação deixa de oferecer proteção
- Expor o token em URL: URLs aparecem em históricos, logs e referers
- Ignorar XSS: um script malicioso na origem legítima pode capturar o token
Checklist antes de publicar
- cookies seguros e SameSite definidos
- operações de escrita usam métodos adequados
- Origin é validado
- token possui alta entropia
- comparação resistente a timing
- falhas não expõem segredos
- rotas sensíveis possuem testes
- proxy e ambientes foram revisados
Referências oficiais
Conteúdos relacionados
- O que é Node.js
- como criar uma API com Node.js
- otimizar APIs RESTful em Node.js
- testes unitários com Jest
Conclusão
A defesa contra CSRF funciona melhor em camadas: cookies adequados, métodos HTTP corretos, validação de origem e token associado à sessão. Nenhuma dessas medidas substitui proteção contra XSS ou autorização no servidor. O objetivo é impedir que outro site produza uma requisição válida em nome de um usuário autenticado.
O caminho mais seguro é implementar uma versão pequena, observável e testável, medir seu comportamento e evoluir com base em dados. Quando contratos, limites e falhas são tratados explicitamente, CSRF em aplicações Node.js deixa de ser apenas uma funcionalidade e se torna uma parte confiável da plataforma.




