Content Security Policy con experimental.csp
La Content Security Policy como segunda muralla que corta la ejecución de scripts no autorizados incluso cuando falla el escape, la feature nativa experimental.csp de Astro que calcula sola los hashes de sus scripts y estilos, la diferencia entre nonces y hashes para autorizar código inline, y el matiz de emitir la política por meta o por cabecera.
Qué corta una CSP y por qué la necesitas
Una Content Security Policy es una lista de reglas que el navegador aplica a una página: qué orígenes pueden servir scripts, estilos, imágenes o conexiones, y qué código inline se permite ejecutar. Su directiva más valiosa es script-src, porque el XSS necesita, en última instancia, ejecutar JavaScript. Si tu política dice que solo corren scripts de tu propio dominio y ninguno inline sin autorizar, un <script>alert(document.cookie)</script> inyectado por un atacante simplemente no se ejecuta: el navegador lo bloquea y lo reporta.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'La idea rectora es defensa en profundidad: no confías en que el escape nunca falle, sino que colocas una segunda barrera que actúa justo cuando la primera se rompe. La CSP no reemplaza al escape ni al saneo —una página bien escrita los necesita igual—, los respalda. Es la diferencia entre una cerradura y una alarma: quieres las dos, porque cada una cubre el fallo de la otra.
Más allá de script-src, unas pocas directivas cargan con casi todo el peso defensivo y conviene fijarlas pronto. object-src 'none' mata los <object> y <embed>, vectores viejos y hoy rara vez necesarios. base-uri 'self' impide que un atacante inyecte un <base> que redirija todas tus URLs relativas hacia su dominio. Y default-src 'self' pone el suelo: lo que no tenga directiva propia hereda esa restricción, de modo que un tipo de recurso que olvidaste no queda abierto por descuido. Una CSP mínima pero seria empieza casi siempre por esas cuatro piezas.
experimental.csp: la feature de Astro
Escribir una CSP a mano es notoriamente frágil, porque un sitio moderno emite scripts y estilos propios cuyo contenido cambia en cada build, y cada uno debe quedar autorizado o la propia página se rompe. Astro 7 resuelve esto de raíz con experimental.csp: al activarlo, calcula el hash criptográfico de cada script y estilo que él mismo genera y los inserta en la política, de modo que tu propio código queda permitido sin que tú lleves esa contabilidad.
// astro.config.mjsimport { defineConfig } from 'astro/config';export default defineConfig({ experimental: { csp: true, },});Con eso basta para empezar: Astro emite una CSP que autoriza exactamente sus artefactos y nada más. Cuando necesites afinarla, csp acepta un objeto con directivas propias, el algoritmo de hash y reglas específicas para scripts y estilos.
export default defineConfig({ experimental: { csp: { algorithm: 'SHA-512', directives: [ "default-src 'self'", "img-src 'self' https://cdn.example.com", ], scriptDirective: { resources: ["'self'"], strictDynamic: true, }, }, },});Lo esencial es que Astro se ocupa de la parte tediosa y peligrosa —autorizar su propio código— y te deja a ti solo las decisiones de política: de qué orígenes externos permites imágenes, fuentes o conexiones. La contabilidad de hashes, la que antes rompía sitios en cada despliegue, desaparece.
Una palabra de ese ejemplo merece glosa: strictDynamic, que activa la directiva strict-dynamic. Su idea es delegar la confianza: un script ya autorizado —por su hash o su nonce— puede a su vez cargar otros scripts, que heredan el permiso sin figurar uno a uno en la política. Resuelve el problema práctico de las librerías que inyectan sus propias dependencias, y lo hace sin volver a una lista de dominios: la confianza fluye desde el código que tú avalaste, no desde un origen apuntado en una lista. Es la forma moderna de una CSP estricta que no se vuelve inmanejable en cuanto entra una dependencia real.
Nonces y hashes: autorizar lo inline
El problema difícil de toda CSP es el código inline: un <script> sin src cuyo contenido vive en el propio HTML. Una política estricta lo prohíbe por defecto, porque es justo la forma del XSS. Para permitir el tuyo sin abrir la puerta al del atacante existen dos mecanismos, y entender su diferencia es entender la CSP entera.
Un hash es la huella criptográfica del contenido exacto del script. La política declara “permito el script cuyo SHA sea este”, y el navegador solo ejecuta el bloque inline cuyo contenido produzca ese hash idéntico. Es determinista y encaja con contenido estático: sirve para código que no cambia entre peticiones, y es justo el mecanismo que Astro calcula solo para sus artefactos.
Content-Security-Policy: script-src 'self' 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='Un nonce es un número aleatorio irrepetible que el servidor genera en cada respuesta, coloca en la cabecera CSP y también como atributo del <script nonce="..."> legítimo. El navegador solo ejecuta los scripts que exhiban el nonce del día; el atacante, que inyecta a ciegas, no puede adivinar el valor de esta petición concreta. Su precio es que exige render bajo demanda —cada respuesta necesita su token fresco— mientras que los hashes funcionan también en páginas estáticas. La regla práctica: hashes para lo que no cambia, nonces para lo que se genera por petición.
Meta o cabecera: dónde vive la política
Hay un último matiz que ahorra depuraciones desconcertantes. Una CSP puede viajar de dos maneras: como una etiqueta <meta http-equiv="content-security-policy"> dentro del <head>, o como una cabecera HTTP Content-Security-Policy en la respuesta. Astro, por defecto, la inserta como meta, lo que tiene la ventaja de funcionar igual en sitios estáticos servidos desde un CDN, sin necesidad de configurar el servidor.
Pero el meta tiene una limitación tajante: algunas directivas solo surten efecto como cabecera y son ignoradas dentro de un meta. Las más importantes son frame-ancestors —que decide quién puede meter tu página en un <iframe>— y report-uri o report-to, que envían informes de violaciones. Si tu política depende de ellas, no basta la meta: has de emitir la CSP como cabecera desde el middleware o el adaptador. Confundir esto lleva a creer que una directiva está protegiendo cuando el navegador la descartó en silencio.