CSP em SPAs React: guia passo a passo para iniciantes
Por John Pires · 23 de março de 2026

CSP em SPAs React: guia prático
Introducao ao problema
O que é CSP e por que importa
Content Security Policy (CSP) é um mecanismo de defesa que reduz o risco de Cross-Site Scripting (XSS) e outros ataques baseados em execução de código malicioso no navegador. Para uma SPA (Single-Page Application) em React, CSP é crítico porque uma única página carrega muitos recursos dinâmicos e frequentemente usa inline scripts ou bibliotecas de terceiros que podem introduzir vetores de execução.
Imagine CSP como um porteiro: ele verifica de onde os scripts, estilos e imagens podem vir. Sem um porteiro, qualquer um pode entrar; com regras frouxas, o ataque ainda passa. Com regras bem definidas, a superfície de ataque diminui.
Como a arquitetura SPA complica CSP
SPAs tipicamente injetam código, usam bundles colocados dinamicamente, carregam web workers, blobs e data: URLs. Além disso, o ambiente de desenvolvimento (dev server, HMR) tem requisitos diferentes do ambiente de produção. Essas características tornam fácil quebrar a aplicação ao aplicar CSP sem planejamento.
Erros comuns: usar 'unsafe-inline' para evitar lidar com nonces/hashes, permitir 'unsafe-eval' porque uma biblioteca usa eval, ou deixar default-src como '*' para "não ter problema". Esses atalhos removem a proteção do CSP.
Pre-requisitos e setup
O que você precisa ter instalado
Antes de começar, certifique-se de ter Node.js (>=14), npm ou yarn, e uma SPA React básica (create-react-app ou equivalente). Também precisaremos de um servidor simples para enviar headers CSP dinâmicos — vou usar Express porque é direto e ensina o essencial.
Se já tem uma SPA pronta, ótimo. Se não, crie uma com create-react-app e rode npm start; mas neste guia a versão final serve via Express para que possamos controlar headers.
Decisões iniciais (headers x meta)
Você pode aplicar CSP via header HTTP (recomendado) ou via meta tag . Use header quando puder controlar o servidor — headers têm precedência e cobrem recursos carregados antes do HTML. Meta tags são úteis em hospedagem estática onde você não pode alterar headers.
Para SPAs com servidor (SSR ou fallback static + backend), prefira headers. Para hospedagem estática (S3, Netlify), use meta e verifique restrições da plataforma.
Tutorial guiado passo a passo
1) Código errado: CSP permissivo (exemplo de code review)
Vou mostrar o jeito errado primeiro — o que vejo com frequência em PRs iniciantes. Esse CSP parece resolver problemas no curto prazo, mas é inseguro.
// Cabeçalho perigoso que vejo em reviews
// Isso permite praticamente tudo: scripts inline, eval e recursos de qualquer origem
res.setHeader('Content-Security-Policy', "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:");
O que acontece depois: a aplicação funciona — inclusive scripts injetados por atacantes podem executar. 'unsafe-inline' e 'unsafe-eval' anulam a maior parte da proteção do CSP. Não use isso em produção.
2) Primeiro passo correto: princípio do menor privilégio
Comece com uma política restritiva e depois libere apenas o necessário. Para uma SPA React típica: permitir apenas 'self' para a maioria dos recursos, bloquear object-src, permitir conexões para APIs específicas e tratar scripts com nonce ou hashes.
Exemplo de política inicial recomendada em modo report-only para teste:
// Exemplo de header inicial (report-only) para testar sem quebrar a app
res.setHeader('Content-Security-Policy-Report-Only',
"default-src 'self'; " +
"script-src 'self' 'nonce-REPLACE_NONCE'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"connect-src 'self' https://api.minhaapi.com; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"report-uri /csp-report");
Contexto: usamos report-only para entender violações sem bloquear. Substitua REPLACE_NONCE dinamicamente (veja passo 4).
3) Implementando nonce dinâmico no servidor (Express)
Vamos implementar um servidor Express simples que gera um nonce por resposta, injeta esse nonce no HTML da SPA e define o header CSP com esse nonce. Assim scripts inline autorizados podem rodar sem 'unsafe-inline'.
// server.js - serve index.html com nonce e header CSP dinâmico
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const app = express();
const indexPath = path.join(__dirname, 'build', 'index.html');
app.use('/static', express.static(path.join(__dirname, 'build', 'static')));
app.get('/', (req, res) => {
const nonce = crypto.randomBytes(16).toString('base64');
let html = fs.readFileSync(indexPath, 'utf8');
// placeholder no index.html: __CSP_NONCE__
html = html.replace('__CSP_NONCE__', nonce);
const policy = "default-src 'self'; " +
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; ` +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"object-src 'none'; " +
"base-uri 'self'; ";
res.setHeader('Content-Security-Policy', policy);
res.send(html);
});
app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/json'] }), (req, res) => {
console.warn('CSP Violation:', JSON.stringify(req.body));
res.status(204).end();
});
app.listen(3000, () => console.log('Server rodando na porta 3000'));
O que faz: gera um nonce, injeta no index.html e envia o header CSP. Depois, qualquer script inline que tenha esse nonce pode executar; scripts sem nonce são bloqueados. Esse padrão é robusto para SPAs servidas por um backend.
4) Ajustando o index.html (React) para usar nonce
Edite o template do seu HTML (build/index.html) para ter um pequeno inline script autorizado que inicializa variáveis ou pega o nonce. No CRA, coloque um placeholder para ser substituído no build ou no servidor.
<!doctype html>
<html lang='pt-br'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width,initial-scale=1' />
<title>Minha SPA</title>
<script nonce='__CSP_NONCE__'>
// script pequeno permitido pelo nonce para passar config inicial
window.__CSP_NONCE__ = '__CSP_NONCE__';
</script>
<link rel='stylesheet' href='/static/css/main.css' />
</head>
<body>
<div id='root'></div>
<script src='/static/js/main.js' defer nonce='__CSP_NONCE__'></script>
</body>
</html>
O que acontece: o servidor substitui todas as ocorrências de __CSP_NONCE__ pelo nonce gerado. O bundle do React (main.js) carrega com defer e o nonce, sendo permitido pelo CSP. Evite inline grandes; prefira bundles externos autorizados via nonce.
5) Report endpoint e modo report-only
Comece em report-only para detectar políticas que bloqueiam recursos legítimos. Mantenha um endpoint para receber reports e automatize alertas.
Tip: registre violações em um sistema de logs estruturados (ELK, Datadog) para rastrear rapidamente regressões após deploy.
Resultado final funcionando
O que está protegido agora
Com o setup acima, você protege a SPA contra a maioria dos XSS baseados em injeção de script: scripts inline não autorizados e blobs maliciosos são bloqueados, objetos executáveis não são carregados e conexões são limitadas a origens esperadas.
Além disso, o uso de nonce por resposta dificulta a reutilização de um script malicioso: o nonce expira quando a resposta expira; um atacante que conseguir inserir um script no HTML renderizado localmente precisaria do nonce correto da resposta — difícil de obter sem comprometer o servidor.
Teste prático: como verificar no navegador
Abra DevTools > Security/CSP ou Network e inspecione o header Content-Security-Policy. Para testes, provoque uma violação (injetando um script inline sem nonce) e verifique se o console reporta a violação ou se o endpoint de report recebeu o JSON de violação.
Se você usou report-only, verá violações sem bloqueio; depois de ajustar a política, remova o report-only para aplicar restrições.
Erros comuns, armadilhas e dicas práticas
Erro comum 1: 'unsafe-inline' e 'unsafe-eval'
Uso fácil, mas perigoso. 'unsafe-inline' permite inline scripts (incluindo os injetados por um atacante). 'unsafe-eval' permite eval, new Function e algumas ferramentas de sourcemap/debug dependem disso. Evite ambos. Em vez de 'unsafe-inline', use nonces ou hashes. Em vez de 'unsafe-eval', atualize bibliotecas ou remova dependências que exigem eval.
Erro comum 2: hashes e a complexidade com bundlers
Hashes (sha256-...) são estáveis para scripts inline imutáveis. Em SPAs com build pipeline que gera conteúdo dinâmico (hashes nos bundles), hashes para scripts inline podem ser difíceis. Nonces são uma alternativa prática porque você pode injetar no servidor e aplicar por resposta.
Dica prática: como lidar com bibliotecas que usam eval
Identifique a biblioteca que exige eval. Procure alternativas que não usem eval, ou carregue essa biblioteca de forma isolada e confine seu uso. Como último recurso, considere um CSP específico para a rota que usa essa biblioteca, mas entenda que relaxar CSP tem custo de segurança.
// Exemplo: detectando uso de eval durante dev
// Adicione este snippet temporário para detectar isNaN calls via eval
(function(){
const originalEval = window.eval;
window.eval = function(code){
console.warn('eval usado:', code.slice(0, 200));
return originalEval(code);
};
})();
Depois de identificar a biblioteca, crie um plano: atualizar, substituir ou isolar. Nunca deixe esse snippet em produção.
Desafio para o leitor praticar
Desafio 1: converter projeto CRA para CSP com nonce
Objetivo: pegue um projeto criado com create-react-app, gere um build, coloque os artefatos em uma pasta build/ e sirva com o Express do exemplo. Implemente a substituição de nonce no build/index.html e valide que o main.js carrega com o nonce. Rode em modo report-only primeiro e corrija violações.
- Gerar build: npm run build
- Implementar server.js conforme exemplo e rodar node server.js
- Inspecionar headers e reports
Dica: automatize a substituição do placeholder no pipeline de deploy, não edite o build manualmente em produção.
Desafio 2: remover 'unsafe-eval' de dependência
Identifique uma dependência que exige eval (ex.: bundles antigos de map libraries). Atualize para versão sem eval ou substitua por alternativa. Documente o processo e execute testes para garantir que o comportamento permanece igual.
Conclusao
Resolver CSP em SPAs React requer disciplina: comece com políticas estritas em modo report-only, gere nonces por resposta e autorize apenas scripts/recursos necessários. Evite atalhos perigosos como 'unsafe-inline' e 'unsafe-eval'.
Trabalhe incrementalmente: monitoramento de reports, ajustar policy e só então aplicar o bloqueio. Em code reviews, critique políticas permissivas e peça justificativas quando ver default-src '*' ou unsafe- flags.
Resumo prático:
- Use headers sempre que possível;
- Gere nonces por resposta para autorizar scripts seguros;
- Comece em report-only e corrija violações antes de bloquear;
- Substitua bibliotecas que pedem eval por alternativas seguras.
Segurança é iterativa. Um CSP bem aplicado reduz muito o risco de XSS em SPAs e aumenta a confiança ao publicar código em produção.
Recursos adicionais
Links de referência para aprofundar (procure as versões oficiais):
- Spec do Content Security Policy (W3C ou MDN)
- Documentação do create-react-app sobre customização do index.html
- Guides sobre report-uri / report-to e como agregá-los em sistemas de logs
Assuntos: csp, React, spa, Segurança, express
Leia também
CSRF em SPAs: SameSite, cookie de duplo envio e tokens — passo a passo
Guia prático e passo a passo para proteger SPAs contra CSRF: configure SameSite, implemente cookie de duplo envio e tokens anti-CSRF com exemplos reais em…