Voltar ao blog

Seu Script de Analytics É o Buraco na Sua Content-Security-Policy

Uma CSP estrita fecha o XSS. Uma tag de analytics de terceiros reabre. Veja por que allowlists de host e a ausência de SRI minam sua política — e o que um tracker first-party resolve.

Uma Content-Security-Policy é a defesa mais eficaz contra cross-site scripting. No momento em que você adiciona uma tag de analytics de terceiros, normalmente precisa abrir um buraco nela. É nesse buraco que vivem a maioria dos bypasses de CSP do mundo real.

A razão é estrutural, não acidental. Scripts de analytics pesados em vigilância são feitos para carregar mais código em tempo de execução, a partir de domínios que você não controla. Uma CSP estrita existe justamente para proibir isso.

Allowlists de host são a forma fraca de confiar em um script

A forma antiga de permitir um fornecedor de analytics era uma allowlist de host:

Content-Security-Policy: script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;

A OWASP não recomenda mais esse padrão. Allowlists são facilmente contornadas: qualquer origem permitida que hospede um endpoint JSONP, um open redirect ou scripts enviados por usuários vira um vetor de XSS. Tag managers pioram a situação, porque existem exatamente para injetar mais scripts a partir de origens arbitrárias.

A prática líder atual é uma CSP estrita construída sobre um nonce por resposta mais strict-dynamic:

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

Com strict-dynamic, o navegador confia em scripts carregados por um script já confiável — que é exatamente o comportamento do qual um tag manager depende. Isso é conveniente para o fornecedor e perigoso para você: uma única tag comprometida agora tem permissão implícita para carregar qualquer coisa.

SRI não funciona nos scripts que mais precisam dele

Subresource Integrity permite que o navegador verifique um arquivo baixado contra um hash criptográfico antes de executá-lo:

<script
  src="https://cdn.example.com/tracker.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

O SRI suporta sha256, sha384 e sha512, e o navegador recusa o recurso em qualquer divergência. É uma defesa limpa contra um CDN comprometido — para arquivos estáticos, com versão fixada.

Ele desmorona com analytics. Tags de fornecedores são deliberadamente mutáveis: o arquivo na mesma URL muda sempre que o fornecedor lança recursos, então um hash fixado quebraria a coleta no próximo deploy. Na prática, os times abrem mão do SRI exatamente nos scripts com maior alcance dentro da página. A recalibração de risco de cadeia de suprimentos de outubro de 2025 classificou a implementação inadequada de SRI como de severidade alta por esse motivo.

Trusted Types elevam o patamar em fevereiro de 2026

A metade do problema relativa a DOM-XSS agora tem uma resposta no nível do navegador. A partir do Baseline de fevereiro de 2026, Trusted Types estão disponíveis nos navegadores atuais. Você ativa a aplicação com:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default;

Depois disso, sinks do DOM como Element.innerHTML rejeitam strings cruas e aceitam apenas valores gerados por uma policy registrada:

const policy = trustedTypes.createPolicy("default", {
  createHTML: (input) => DOMPurify.sanitize(input),
});
el.innerHTML = policy.createHTML(userInput); // ok
el.innerHTML = userInput;                     // throws TypeError

Isso é genuinamente forte. Também é o tipo de regra que código legado de analytics e tag manager viola rotineiramente, porque esses scripts escrevem em innerHTML e injetam nós <script> como strings simples. Ligar Trusted Types muitas vezes significa desligar seu fornecedor de analytics primeiro.

Um tracker first-party se encaixa numa política estrita em vez de brigar com ela

O conflito desaparece quando o script de analytics é pequeno, autossuficiente e servido a partir da sua própria origem. Um tracker que não carrega nenhum código adicional não precisa nem de strict-dynamic nem de um host de fornecedor na sua allowlist:

Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script';

É isso. Nenhuma origem de terceiros, nenhuma escalada de nonce, nenhuma exceção aberta para um tag manager.

O tracker da Monoid tem cerca de 2 KB, não tem dependências e nunca chama um sink de DOM-XSS. Ele não escreve innerHTML, não injeta scripts e não lê nem define cookies ou storage. Ele envia uma requisição keepalive por pageview para /collect e fora isso fica ocioso:

fetch('/collect', {
  method: 'POST',
  body: JSON.stringify({ site_id, path, referrer, screen_w }),
  keepalive: true,
});

Como o arquivo é estático, você pode aplicar SRI a ele sem que isso jamais quebre — um hash fixado continua válido até você decidir atualizar o script. E como a identidade é um hash diário de mão única, SHA-256(IP | UA | SALT_SECRET | YYYY-MM-DD), não há perfilamento cross-site que uma CSP relaxada pudesse sequer vazar.

O padrão se generaliza para além de analytics: cada script que você remove da sua allowlist é uma classe de bypass de XSS que você remove junto. O script de terceiros mais seguro é aquele que você não carrega.

Fontes

Comments

Loading comments…