CSP estrita: como reduzir o risco de XSS sem transformar o deploy em um campo minado

Content Security Policy, ou CSP, não corrige uma falha de XSS. Ela diminui a chance de que uma falha já existente consiga executar script no navegador do usuário. Essa diferença parece burocrática, mas evita duas armadilhas: tratar o header como item de checklist e achar que um allowlist enorme de domínios equivale a uma política forte.

Uma CSP baseada em nonces ou hashes permite que o navegador execute apenas scripts que receberam uma prova explícita de confiança. É uma camada de defesa em profundidade. Validação de entrada, escaping por contexto e correção da vulnerabilidade continuam obrigatórios; a CSP entra para tornar a exploração mais difícil caso uma dessas barreiras falhe.

Por que listas de domínios não bastam

Uma política como script-src 'self' https://cdn.exemplo.com parece restritiva, até o projeto adicionar analytics, tag manager, widget de suporte, mapas e mais cinco integrações. A lista cresce, os scripts permitidos carregam outros scripts e a auditoria vira uma caça a URLs. A documentação do web.dev alerta que CSPs baseadas apenas em allowlists de host frequentemente podem ser contornadas.

Uma CSP estrita troca a pergunta “de qual domínio pode vir?” por “este script foi autorizado nesta resposta?”. Em páginas dinâmicas, isso geralmente significa gerar um nonce criptograficamente aleatório para cada resposta, colocá-lo no header e nos elementos script confiáveis.

Content-Security-Policy:
  script-src 'nonce-R4nd0mValue' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none'

O nonce do exemplo é só ilustrativo. No servidor ele precisa ser imprevisível e novo a cada resposta. Reutilizá-lo anula boa parte da proteção, então não coloque uma string fixa em variável de ambiente e declare vitória antes do café.

O papel de strict-dynamic

Com 'strict-dynamic', a confiança dada por nonce ou hash a um script raiz pode ser propagada para scripts que ele carregar dinamicamente. Isso é útil para bundles, loaders e integrações de terceiros que injetam dependências. Sem isso, o script raiz autorizado pode falhar ao montar o restante da aplicação.

Há uma contrapartida importante: confiar em um loader significa confiar no que ele decide carregar. Se esse código cria tags script a partir de dados controláveis por atacante, strict-dynamic amplia o estrago. A política continua exigindo que a aplicação trate URLs, HTML e entradas como dados hostis.

Adoção sem derrubar a aplicação

Comece com Content-Security-Policy-Report-Only. Nesse modo, o navegador registra violações sem bloquear recursos. É a maneira mais segura de descobrir scripts inline esquecidos, eventos onclick, eval(), fontes, imagens e chamadas de API que seu app faz fora do caminho feliz.

  • Inventarie scripts, estilos e conexões realmente necessários.
  • Remova handlers inline e mova comportamento para addEventListener().
  • Gere nonce no servidor, ou use hashes para conteúdo estático.
  • Observe os relatórios e corrija exceções com causa conhecida.
  • Só então ative o header em modo de bloqueio.

Também não esqueça de diretivas que protegem fora de script-src. object-src 'none' corta plugins legados; base-uri 'none' impede injeção de base; frame-ancestors controla quem pode embutir sua página. Cada uma resolve um pedaço de superfície que uma política focada apenas em scripts deixa exposto.

Uma CSP forte costuma revelar dívida técnica: templates com script inline, dependências que usam avaliação dinâmica e widgets externos opacos. Isso é desconfortável, mas útil. O header deixa de ser um enfeite de segurança quando vira um inventário honesto do código que recebe permissão para rodar no navegador.

O nonce precisa atravessar a renderização sem virar dado global

Gere o nonce no início de cada resposta, use a mesma string no header e nos blocos confiáveis daquela resposta e descarte-a no fim. Em SSR, passe o valor pelo contexto de renderização. Não o coloque em cache compartilhado nem em um singleton que sobreviva a requisições.

$nonce = base64_encode(random_bytes(16));

header(
    "Content-Security-Policy: " .
    "script-src 'nonce-{$nonce}' 'strict-dynamic'; " .
    "object-src 'none'; base-uri 'none'"
);

render('page.php', ['cspNonce' => $nonce]);

Se HTML inteiro passa por CDN, um nonce por resposta conflita com cache estático. Nesse caso, hashes para scripts imutáveis, fragmentação do cache ou injeção controlada na borda podem se encaixar melhor. O desenho de cache deve ser escolhido junto com a política, não remendado depois.

Relatórios precisam virar correção, não coleção de ruído

Envie violações por report-to ou report-uri para um endpoint com limite de volume. Remova parâmetros sensíveis, agrupe por diretiva, origem e release e acompanhe a taxa, não cada evento isolado. Extensões de navegador e scripts injetados podem gerar ruído que não pertence à aplicação.

CSP também não decide como inserir conteúdo dinâmico. O guia sobre escaping, sanitização e Trusted Types para prevenir XSS cobre essa fronteira: texto, atributo, URL e HTML têm destinos diferentes. Trusted Types ajuda a controlar sinks de DOM; CSP limita a execução quando outra barreira falha.

  • mantenha uma política de fallback para navegadores sem strict-dynamic quando o público exigir;
  • evite adicionar 'unsafe-inline' para corrigir uma única integração;
  • teste login, checkout, consentimento e recuperação de erro no modo Report-Only;
  • ative bloqueio por percentual ou rota antes de cobrir o site inteiro.

Fontes