Saltar al contenido
Development

Convertir un hash de CSP en una salida del build en vez de mantenerlo a mano

Por Victor Da Luz
astrosecuritydev-logsite

Encontré este error por accidente, mientras verificaba un cambio de analítica completamente sin relación. Una pasada de browser-verify sacó a la luz una violación de Content-Security-Policy que no tenía nada que ver con lo que se estaba probando. Podría haberlo descartado como ruido. En cambio lo rastreé, y resultó que el botón “Launch Planetary Pathways” de la portada llevaba dos días muerto en producción, sin ningún error visible para quien no estuviera mirando las herramientas de desarrollo.

El error: un hash que fue cierto una vez

La CSP de este sitio permite exactamente un script en línea por hash sha256, el manejador de clic en GamesSection.astro que revela el iframe del juego. Ese hash se fijó una vez, cuando se agregaron por primera vez los encabezados de seguridad, y nadie lo había tocado desde entonces. Después una pasada de i18n sin relación sobre ese mismo componente cambió su salida compilada en unos pocos bytes. El hash dejó de coincidir. astro check siguió en verde. npm run build siguió en verde. El único lugar donde esto se notaba era una consola del navegador, en una página donde nadie tenía abiertas las herramientas de desarrollo.

Dos días es mucho tiempo para que una funcionalidad real esté rota en silencio sin que nada lo detecte.

¿Por qué un script recibe hash y otro no?

Este sitio también tiene scripts que no necesitan hash, los scripts de la puerta de consentimiento de @vdaluz/astro-opt-in-analytics se compilan en archivos externos en su lugar, lo que evita el problema por completo. Al principio asumí que esto tenía que ver con las declaraciones import: los scripts que importan algo se empaquetan externamente, los scripts simples se quedan en línea. Suposición equivocada. La propia documentación de Astro da la regla real: los scripts pequeños ya procesados se insertan directamente en el HTML “para reducir la cantidad de solicitudes.” No hay un umbral de tamaño documentado, ni una bandera de configuración para desactivarlo. El script de lanzamiento del juego es diminuto, así que queda en línea. Los scripts de la puerta de consentimiento arrastran suficiente código a través de sus imports como para cruzar el umbral que sea que Astro use internamente, así que terminan externalizados como efecto secundario de ser más grandes, no porque nadie lo haya pedido.

Esa distinción importa porque significa que no se puede forzar de manera confiable la externalización de un script solo con reestructurarlo. Y si un script se queda en línea, cualquier hash fijado a mano para él está a un solo cambio de quedar desactualizado, en silencio, para siempre.

El arreglo: dejar de mantener el hash a mano por completo

Recalcular el hash una vez habría arreglado el error de hoy y habría dejado preparado exactamente el mismo error para la próxima vez que se toque este componente. Así que en vez de un parche, escribí una pequeña integración de Astro que corre después de cada build: escanea el HTML generado real en busca de scripts en línea, calcula el hash de lo que encuentra, y escribe esos hashes en el archivo _headers desplegado. El hash de CSP ya no es algo que alguien tiene que recordar actualizar, es una salida del build, calculada de nuevo cada vez, que coincide con lo que Astro realmente publicó.

Construirla sacó a la luz algo más que vale la pena saber: la primera versión también calculaba el hash de un archivo completamente sin relación, el propio HTML del build de Unity WebGL incluido, que ya tiene su propio override de CSP separado con unsafe-inline. El escaneo necesitó una exclusión explícita para esa ruta, o cada actualización del motor de Unity agregaría en silencio un hash extraviado a la política del sitio principal sin ningún motivo.

Lo que seguiría haciendo

El servidor de desarrollo casi me mandó también por el camino equivocado esta vez, una verificación de CSP contra npm run dev lanza una violación completamente distinta y sin relación, porque Astro empaqueta los scripts de otra forma en modo desarrollo. Si hubiera confiado en ese primer resultado habría perseguido un fantasma. Construir y previsualizar la salida real de producción antes de confiar en un resultado de “esto ya se ve arreglado” es lo que realmente detectó tanto el error original como el primer error de mi propia integración.

Lecturas relacionadas

Development

El error de CSP que no me correspondía arreglar

Una lista de lanzamiento, una violación de Content Security Policy que señalaba el script exacto que acababa de editar, y un comentario en el repositorio que habría evitado todo el desvío.

Leer
Development

La CSP que solo se rompía en producción

Cuatro fallas detrás de un solo encabezado: middleware muerto, reglas de `_headers` que se combinan en vez de sobrescribirse, un worker de URL blob bajo script-src, y un script que solo inyecta el edge.

Leer