Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 077 — Política de seguridad de contenido#

⬅️ 076 · 📚 Parte 5 · 🎓 Clases · 078 ➡️ Parte 5 — Identidad y seguridad · Nivel 🔴 avanzado · Pista frontendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Limitar lo que el navegador acepta ejecutar. La clase 073 mostró que estos frameworks escapan por omisión; esta clase empieza donde aquella falla: alguien usó la puerta explícita con contenido ajeno y el script entró. La política de seguridad de contenido es la red que hay debajo [owasp-cheatsheets].

🧩 La situación#

La página trae dos scripts:

Con la política activa, el navegador ejecuta el primero y se niega a ejecutar el segundo. La ruta /sin-politica sirve exactamente la misma página sin la cabecera: es la comparación.

🧮 El contrato#

Petición Respuesta Qué mide
GET / script-src con 'nonce-…', sin unsafe-inline ni unsafe-eval, y el marcado lleva ese mismo nonce la política real y su coherencia
GET / base-uri 'none', object-src 'none' las puertas traseras del nonce, cerradas
GET / el marcado contiene <script>robar()</script> el XSS sí entró
GET / otra vez la política no repite el nonce anterior un nonce por petición
GET /sin-politica sin cabecera, mismo script inyectado la diferencia

El primer caso mide de una vez las dos mitades que hay que hacer coincidir: la cabecera dice 'nonce-ABC' y el <script> del marcado dice nonce="ABC". Si no coinciden —el error número uno al desplegar CSP— la política bloquea al script bueno, la página se rompe, y la reacción habitual es añadir unsafe-inline, que desactiva la defensa entera.

El cuarto caso mide que el nonce es de un solo uso. Un nonce fijo en la configuración no es un nonce: el atacante lo lee en el HTML de ayer y lo escribe en su script.

🔬 Qué mide este contrato y qué no#

Con honestidad, porque es la mitad del valor de la clase:

Lo que sí puede decirse con la medición hecha: con esta política, un navegador conforme no ejecuta el script inyectado — porque no lleva el nonce y no hay unsafe-inline que lo salve [whatwg-html].

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Política de seguridad de contenido (CSP) Una cabecera que le dice al navegador qué puede cargar y ejecutar. Es la red que hay debajo cuando el escapado falla: el script inyectado está en el marcado y el navegador se niega a ejecutarlo.
Nonce Un valor aleatorio por respuesta que autoriza a un script concreto dentro de una política de seguridad de contenido. Un nonce fijo en la configuración no es un nonce: el atacante lo lee en el HTML de ayer.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
React biblioteca de interfaz de JavaScript/TypeScript (JavaScript) 2013 MIT Meta y colaboradores
Vue framework de interfaz de JavaScript/TypeScript (JavaScript) 2014 MIT proyecto independiente
Svelte framework de interfaz de JavaScript/TypeScript (JavaScript) 2016 MIT proyecto independiente
SolidJS biblioteca de interfaz de JavaScript/TypeScript (JavaScript) 2018 MIT proyecto independiente

🔧 React#

Impuso la idea de la interfaz como función del estado y el árbol virtual. Es una biblioteca, no un framework: no arranca tu aplicación ni define su ciclo de vida.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Vue#

Adopción progresiva: sirve como etiqueta en una página existente o como framework completo. Su reactividad fina influyó en toda la generación siguiente.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 Svelte#

Mueve el trabajo del navegador al compilador. Cambia el coste de ejecución por dependencia de la fase de construcción: un compromiso, no una mejora gratuita.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
Pagina.svelte componente de Svelte
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 SolidJS#

Reactividad de grano fino sin árbol virtual: el componente se ejecuta una vez y solo se actualiza lo que leyó el valor cambiado.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts
pnpm exec babel App.jsx --out-file App.compilada.mjs

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
.babelrc configuración de Babel: qué transformación se aplica al compilar
App.jsx componente en JSX
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Las cuatro emiten la misma política y la misma página. La cabecera la pone la capa HTTP, que es donde vive CSP: no es una característica del framework de vistas, y esa es la primera lección de la clase.

Las cuatro parten del mismo desastre — el de la clase 073, consumado:

const INYECTADO = "<script>robar()</script>";

El escapado falló. Alguien usó la puerta explícita con contenido de un usuario y el script está en el marcado. Lo que se mide aquí es la red que hay debajo.

La política, idéntica en las cuatro · react/server.mjs#

function politica(nonce) {
  return [
    "default-src 'self'",
    `script-src 'nonce-${nonce}'`,
    "base-uri 'none'",
    "object-src 'none'",
  ].join("; ");
}
    const nonce = crypto.randomBytes(16).toString("base64url");

Un nonce por petición, del generador criptográfico. Un nonce fijo en la configuración no es un nonce: el atacante lo lee en el HTML de ayer y lo escribe en su script.

Y las dos últimas directivas son las puertas traseras conocidas de una política basada en nonce: <base> reescribe a dónde apuntan las rutas relativas, y un <object> ejecuta contenido sin pasar por script-src. Una política que solo declara script-src deja las dos abiertas.

React · react/server.mjs#

        h("script", { nonce, dangerouslySetInnerHTML: { __html: "window.saludo=1" } }),

nonce es un atributo normal: el renderizador de servidor lo emite sin ceremonia. Nada especial que aprender, que es justo lo que se quiere de un framework aquí.

Vue · vue/server.mjs#

        h("script", { nonce, innerHTML: "window.saludo=1" }),

Lo mismo con innerHTML en vez de la envoltura de React — la diferencia de nombres de la clase 073, otra vez.

Solid · solid/App.jsx#

      <script nonce={nonce} innerHTML="window.saludo=1" />

JSX directo. Tres de los cuatro resuelven esto en una línea sin pensar.

Svelte · svelte/Pagina.svelte — el caso raro, y por qué#

  const legitimo = `<scr${"ipt"} nonce="${nonce}">window.saludo=1</scr${"ipt"}>`;
{@html legitimo}

Ese <scr${"ipt"}> partido en dos no es un truco gratuito: es la única forma de que el compilador de Svelte no se quede con la etiqueta. Y hay dos rarezas detrás, las dos del compilador:

  1. Svelte se apropia de los <script> de la plantilla — son el bloque de script del componente. Un script destinado al navegador hay que emitirlo por la vía cruda, {@html}, que es precisamente la puerta que la clase 073 señalaba como peligrosa. Aquí es la única salida.
  2. Valida el anidamiento HTML en tiempo de compilación. Un <div> dentro de <html> es un error de compilación, no una advertencia en consola. Los otros tres del elenco lo renderizan sin protestar.

Las dos juntas explican una decisión de arquitectura real: SvelteKit no deja la política en manos del componente y la genera él. Cuando el compilador se interpone entre lo que escribes y el marcado, la capa que emite cabeceras tiene que estar por encima del componente, no dentro.

La página sin red debajo#

  if (peticion.url === "/sin-politica") {

Las cuatro sirven la misma página en una segunda ruta, sin la cabecera. Es lo que convierte la clase en una medición y no en una declaración: el mismo marcado, el mismo script inyectado, y la única diferencia es la política. Sin ese segundo caso, un contrato que solo comprobara / no distinguiría una política que funciona de una política que no hace nada.

📊 Comparación#

Framework El nonce en el marcado Su meta-framework
React atributo directo Next.js: nonce por petición desde el middleware
Vue atributo directo Nuxt: módulo de seguridad con nonce y hashes
Solid atributo directo SolidStart: cabeceras por ruta
Svelte por la vía cruda ({@html}) SvelteKit: kit.csp genera hashes y nonces solo

La fila de Svelte resume la clase: el framework de vistas es el peor lugar para gestionar CSP, y por eso el ecosistema lo resuelve un nivel más arriba. SvelteKit es el más explícito al respecto —calcula los hashes de lo que él mismo emite—, y es la respuesta correcta: quien genera el marcado es quien puede saber qué scripts son legítimos.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 077

Los casos están en contrato.json. El verificador ejecuta las implementaciones que encuentre y declara las que omitió.

🧪 Reto de transferencia#

Añade /informe, la ruta que recibe los informes de violación, y sirve la página con Content-Security-Policy-Report-Only apuntando a ella (report-to). Después añade el caso que lo mide: con Report-Only, la cabecera de bloqueo no está presente — es decir, comprueba que estás midiendo antes de romper, que es el orden correcto de un despliegue de CSP.

🔗 Enlaces#

Fuentes#