Volver al blog

Tu script de analítica es el agujero en tu Content-Security-Policy

Una CSP estricta cierra el XSS. Un tag de analítica de terceros lo vuelve a abrir. Por qué las allowlists de hosts y la falta de SRI debilitan tu política.

Una Content-Security-Policy es la defensa más eficaz que existe contra el cross-site scripting. En el momento en que añades un tag de analítica de terceros, normalmente tienes que abrirle un agujero. Ese agujero es donde viven la mayoría de los bypasses de CSP del mundo real.

La razón es estructural, no accidental. Los scripts de analítica con vocación de vigilancia están diseñados para cargar más código en tiempo de ejecución, desde dominios que tú no controlas. Una CSP estricta existe precisamente para prohibir exactamente eso.

Las allowlists de hosts son la forma débil de confiar en un script

La forma antigua de permitir un proveedor de analítica era una allowlist de hosts:

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

OWASP ya no recomienda este patrón. Las allowlists se eluden con facilidad: cualquier origen permitido que aloje un endpoint JSONP, una redirección abierta o scripts subidos por usuarios se convierte en un vector de XSS. Los tag managers lo empeoran, porque existen precisamente para inyectar más scripts desde orígenes arbitrarios.

La práctica líder actual es una CSP estricta construida sobre un nonce por respuesta más strict-dynamic:

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

Con strict-dynamic, el navegador confía en los scripts cargados por un script en el que ya confía, que es exactamente el comportamiento del que depende un tag manager. Eso es cómodo para el proveedor y peligroso para ti: un tag comprometido tiene ahora permiso implícito para cargar cualquier cosa.

SRI no funciona en los scripts que más lo necesitan

La Subresource Integrity permite al navegador verificar un archivo descargado contra un hash criptográfico antes de ejecutarlo:

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

SRI admite sha256, sha384 y sha512, y el navegador rechaza el recurso ante cualquier discrepancia. Es una defensa limpia contra un CDN comprometido, para archivos estáticos y fijados a una versión.

Se desmorona con la analítica. Los tags de los proveedores son deliberadamente mutables: el archivo en la misma URL cambia cada vez que el proveedor lanza funcionalidades, por lo que un hash fijado rompería la recolección en el siguiente despliegue. En la práctica, los equipos descartan SRI justo en los scripts con mayor alcance dentro de la página. La recalibración de octubre de 2025 del riesgo de la cadena de suministro calificó la implementación insegura de SRI como de gravedad alta por esta razón.

Trusted Types elevan el listón en febrero de 2026

La mitad del problema correspondiente al DOM-XSS tiene ahora una respuesta a nivel de navegador. Desde Baseline febrero de 2026, los Trusted Types están disponibles en los navegadores actuales. Habilitas el cumplimiento con:

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

Tras eso, los sinks del DOM como Element.innerHTML rechazan las cadenas en crudo y solo aceptan valores producidos por una política registrada:

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

Esto es genuinamente potente. También es el tipo de regla que el código de analítica heredado y de los tag managers infringe de forma rutinaria, porque esos scripts escriben en innerHTML e inyectan nodos <script> como cadenas planas. Activar Trusted Types a menudo significa desactivar primero tu proveedor de analítica.

Un tracker first-party encaja en una política estricta en lugar de pelear con ella

El conflicto desaparece cuando el script de analítica es pequeño, autocontenido y se sirve desde tu propio origen. Un tracker que no carga código adicional no necesita ni strict-dynamic ni un host de proveedor en tu allowlist:

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

Eso es todo. Ningún origen de terceros, ninguna escalada de nonce, ninguna excepción tallada para un tag manager.

El tracker de Monoid pesa aproximadamente 2 KB, no tiene dependencias y nunca llama a un sink de DOM-XSS. No escribe en innerHTML, no inyecta scripts y no lee ni establece cookies ni almacenamiento. Envía una petición keepalive por página vista a /collect y, por lo demás, permanece inactivo:

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

Como el archivo es estático, puedes aplicarle SRI sin que jamás se rompa: un hash fijado sigue siendo válido hasta que decides actualizar el script. Y como la identidad es un hash diario unidireccional, SHA-256(IP | UA | SALT_SECRET | YYYY-MM-DD), no existe ningún perfilado entre sitios que una CSP relajada pudiera siquiera filtrar.

El patrón se generaliza más allá de la analítica: cada script que eliminas de tu allowlist es una clase de bypass de XSS que eliminas con él. El script de terceros más seguro es el que no cargas.

Fuentes

Comments

Loading comments…