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.
Escapar y sanear son la primera muralla contra el XSS, pero ninguna muralla es perfecta: basta un set:html mal auditado, una dependencia comprometida o un descuido para que un script hostil aterrice en la página. La Content Security Policy es la segunda línea, la que asume que la primera puede caer. Es un contrato que la página le declara al navegador —“solo ejecuta scripts de estas fuentes, y de ninguna otra”— de modo que, aunque el atacante logre inyectar su etiqueta, el navegador se niegue a correrla. Astro 7 convierte esa defensa, tradicionalmente ardua de mantener, en una feature que se activa con una línea.
- Entender la CSP como defensa en profundidad que sobrevive a un fallo del escape.
- Activar la feature nativa con
experimental.cspy ver cómo Astro calcula los hashes por ti. - Distinguir nonces de hashes como mecanismos para autorizar scripts y estilos inline.
- Conocer el matiz de emitir la política por etiqueta
metafrente a por cabecera HTTP.
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.mjs
import { 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.
Una política estricta bloquea, pero si no te enteras de qué bloqueó, no sabes si protege o si simplemente rompe. La directiva report-to, con su endpoint asociado, hace que el navegador te envíe un informe cada vez que una regla se viola: un script que no cuadraba, un origen no autorizado, un estilo inline heredado. En producción, ese caudal de informes es oro —te muestra ataques reales, integraciones olvidadas y errores de tu propia política—, pero solo llega como cabecera, nunca desde una meta, otra razón para emitir la CSP desde el servidor cuando quieras vigilarla. Sin telemetría, endureces a ciegas y descubres los agujeros por accidente.
flowchart TD
BUILD[astro calcula hashes de sus scripts] --> POL[compone la politica CSP]
POL --> HEAD[la inserta como meta en el head]
HEAD --> NAV[el navegador lee la politica]
NAV --> CHK{el script esta autorizado}
CHK -->|hash o nonce coincide| RUN[se ejecuta]
CHK -->|script inyectado sin permiso| BLOCK[bloqueado y reportado]
style RUN fill:#a6e3a1,color:#11111b
style BLOCK fill:#f38ba8,color:#11111bUna CSP estricta desplegada de golpe rompe cosas: un script de terceros olvidado, un estilo inline heredado. El camino sensato es medir antes de bloquear. La cabecera Content-Security-Policy-Report-Only aplica exactamente tu política pero sin bloquear nada: solo reporta lo que habría bloqueado. Despliégala así, recoge los informes durante unos días, ajusta las directivas hasta que el ruido desaparezca, y solo entonces cambia a modo de bloqueo real. Así endureces sin cortarle el sitio a nadie por sorpresa.
La tentación cuando algo inline no corre es añadir 'unsafe-inline' a script-src. Resístela: esa palabra autoriza cualquier script inline, incluido el que un atacante inyecte, y con ello desarma justo la protección que fuiste a buscar. La respuesta correcta es autorizar tu bloque concreto por su hash o su nonce, no abrir la categoría entera. Una CSP con 'unsafe-inline' en scripts es, para el XSS, casi como no tener CSP.
Defensa en profundidad
La CSP actua cuando el escape falla. Bloquea la ejecucion del script inyectado aunque llegue a la pagina.
experimental.csp
Se activa con csp true. Astro calcula los hashes de sus propios scripts y estilos y los autoriza solo.
Hash vs nonce
Hash para contenido fijo y estatico. Nonce, un token por respuesta, para lo generado bajo demanda.
Meta o cabecera
Astro emite por meta. Pero frame-ancestors y los informes solo valen como cabecera HTTP.
La Content Security Policy encarna, en el plano del navegador, el mismo salto conceptual que separa la seguridad ingenua de la madura: el paso de enumerar lo prohibido a enumerar lo permitido. La web nació con un modelo implícito de lista negra para el código: cualquier script que apareciera en la página se ejecutaba, y la defensa consistía en intentar impedir que apareciera nada malo —escapar, sanear, filtrar—. Ese modelo es estructuralmente perdedor, porque exige anticipar todas las formas del ataque, y basta que una se te escape para que caiga la casa. La CSP invierte la carga: en vez de perseguir lo malo, declaras lo bueno —estos orígenes, estos hashes, este nonce— y el navegador prohíbe todo lo demás por omisión. Es el mismo principio que rige un cortafuegos que deniega por defecto, un saneador por lista de permitidos, un sistema de capacidades donde nada puede actuar sin una autorización explícita: el default deny como postura, la idea de que lo seguro es lo que no está permitido hasta que alguien, a conciencia, lo permite. Y hay una segunda lección, más sutil, escondida en el mecanismo del hash y el nonce: ambos son formas de vincular la autorización a la identidad exacta del código. Un hash dice “autorizo este contenido y solo este, byte a byte”; un nonce dice “autorizo lo que yo mismo marqué en esta respuesta”. En los dos casos, la confianza no se concede a una categoría difusa —“los scripts inline”— sino a un artefacto identificado sin ambigüedad. Ahí está la razón profunda de por qué 'unsafe-inline' es veneno: reintroduce la confianza por categoría, deshaciendo el trabajo de vincular permiso a identidad. Que Astro calcule los hashes de sus propios scripts es, entonces, mucho más que una comodidad: es el framework asumiendo la parte más difícil de un modelo de lista blanca —mantener el inventario exacto de lo propio al día en cada build— para que tú solo decidas la política. La defensa en profundidad no es acumular barreras al azar; es reconocer que ninguna capa es infalible y diseñar la siguiente para que corte precisamente lo que la anterior dejó pasar. La CSP es esa siguiente capa, y su forma —permitir lo nombrado, negar lo demás— es la forma que toda seguridad seria termina adoptando.
- Activa
experimental.cspconcsp: trueen la configuración e inspecciona el HTML servido: localiza la etiquetametacon la política y los hashes que Astro insertó por sus scripts. - Añade un
<script>inline propio sin autorizar y comprueba en la consola del navegador que la CSP lo bloquea; luego autorízalo por su hash y verifica que ya corre. - Amplía la política con una directiva
img-srcque permita un CDN externo y confirma que las imágenes de otro origen sin autorizar quedan bloqueadas. - Despliega primero en
Content-Security-Policy-Report-Only, recoge qué habría bloqueado, y razona qué directivas necesitarías como cabecera —no comometa— para queframe-ancestorssurta efecto.