EJEMPLOS DE CSP Y DESPLIEGUE

Ejemplos de Content-Security-Policy: cómo crear una CSP estricta sin romper tu web

La cabecera Content-Security-Policy le dice al navegador qué scripts pueden ejecutarse en tus páginas. Aquí tienes tres políticas de ejemplo que funcionan, lo que hace estricta a una política (nonces, hashes y strict-dynamic), las directivas que default-src no cubre, cómo recibir informes de infracciones y un despliegue por etapas que empieza en modo Report-Only.

Ilustración conceptual de una garita de cerámica marfil con un rastrillo de vidrio: las esferas de vidrio con un sello menta pasan y brillan, mientras una esfera gris sin sello se detiene fuera junto a un sello de cobre.
Ilustración conceptual

La Content-Security-Policy (CSP) es una cabecera de respuesta que indica al navegador qué scripts, estilos, imágenes, marcos y conexiones puede usar una página. Si alguien consigue colar un script en tu HTML, una buena política impide que el navegador lo ejecute. Las políticas copiadas de un foro suelen fallar de dos maneras: rompen la analítica, las fuentes web y el widget de pago, o acaban llenas de 'unsafe-inline' y comodines hasta permitir casi todo.

Esta guía reúne ejemplos que funcionan para tres tipos de sitio, explica qué hace estricta a una política (nonces, hashes y 'strict-dynamic'), repasa las directivas a las que no llega default-src, muestra cómo recoger informes de infracciones y propone un despliegue que empieza en modo Report-Only para que nada se rompa sin aviso. Todos los ejemplos usan example.com; sustituye los valores de ejemplo por los tuyos.

Alcance de esta guía

El comprobador gratuito lee las cabeceras de una sola respuesta pública: la página de inicio del origen que escribes, tras hasta cinco redirecciones. Solo lee la cabecera CSP aplicada, no una política Report-Only, no valora si la política encaja con tu sitio y no es un pentest.

1. Qué hace una Content-Security-Policy y qué no hace

Una política es una lista de directivas separadas por punto y coma. Cada directiva nombra un tipo de recurso y los orígenes permitidos para él: script-src para JavaScript, style-src para CSS, img-src, font-src, connect-src para conexiones fetch, XHR y WebSocket, y frame-src para los marcos que inserta la página. default-src actúa como valor de reserva para estas directivas de carga cuando falta una concreta (W3C CSP Level 3).

Hay tres directivas importantes que no heredan nada de default-src: frame-ancestors (qué sitios pueden enmarcar tu página), base-uri (qué puede fijar un elemento <base>) y form-action (adónde pueden enviarse los formularios). Por eso una política que solo diga default-src 'self' sigue dejando que cualquier sitio meta tu página en un iframe y que una etiqueta <base> inyectada desvíe las URL relativas.

Los valores de origen son de tres tipos:

  • Palabras clave entre comillas simples: 'self' (el propio origen de la página), 'none' (nada), 'unsafe-inline', 'unsafe-eval' y 'strict-dynamic'.
  • Hosts y esquemas: https://cdn.example.com, https://*.example.com, https: o data:.
  • Nonces y hashes: 'nonce-…' y 'sha256-…', que autorizan elementos script o style concretos en lugar de hosts enteros.

Envía la política como cabecera HTTP. Una etiqueta <meta http-equiv="Content-Security-Policy"> sirve para la mayoría de directivas, pero la especificación excluye ahí frame-ancestors, report-uri y sandbox, y una política Report-Only no puede entregarse por meta de ninguna forma (guía CSP de MDN). Conviene tener expectativas realistas: la CSP limita lo que puede hacer el código inyectado, pero no sustituye al escapado de la salida ni a la validación de la entrada, que son lo que evita la inyección.

2. Tres ejemplos de Content-Security-Policy para empezar

Ejemplo A: un sitio sin scripts de terceros. Una política de lista blanca funciona cuando todos los scripts y hojas de estilo son archivos de tu propio origen:

Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests

Bloquea cualquier <script> en línea, cualquier atributo style y todo lo que venga de otros hosts, así que encaja en sitios hechos a mano o generados de forma estática y sin contenido incrustado.

Ejemplo B: un sitio renderizado en el servidor con nonce. Es la política estricta que web.dev recomienda para páginas que el servidor genera en cada petición, con protección de marcos añadida:

Content-Security-Policy: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'

El nonce cambia en cada respuesta (sección 3). Fíjate en lo que deja fuera: al no haber default-src, estilos, imágenes, fuentes y conexiones quedan sin restricción. Es una decisión consciente: se centra en la ejecución de scripts, donde el cross-site scripting hace más daño, y evita listas de hosts que se rompen cada vez que un proveedor cambia de dominio.

Ejemplo C: páginas estáticas o en caché con hashes. Si todo el mundo recibe el mismo HTML, autoriza los scripts en línea por su hash:

Content-Security-Policy: script-src 'sha256-BASE64_HASH_OF_INLINE_LOADER' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'
Tipo de políticaEncaja conTrabajo continuoCuidado con
A: lista de hostsSitios estáticos sin código de tercerosActualizar la lista al añadir un proveedorCualquier script de un host permitido puede cargarse, incluidas versiones antiguas de librerías en un CDN compartido
B: nonce + strict-dynamicPáginas que tu aplicación genera en cada peticiónAñadir el nonce a cada etiqueta script en las plantillasCachés de página que sirven el mismo nonce a todos los visitantes
C: hash + strict-dynamicHTML estático, prerenderizado o en caché del CDNRecalcular los hashes cuando cambia el código en líneaMinificadores y plantillas que alteran los espacios y, con ellos, el hash

Elijas la que elijas, añade las directivas de informes de la sección 6 y envíala primero como Content-Security-Policy-Report-Only.

3. Nonces y hashes en lugar de 'unsafe-inline'

'unsafe-inline' permite cualquier script en línea, también el que inyecte un atacante, y con eso se pierde casi todo lo que la CSP aporta frente al cross-site scripting. Los nonces y los hashes solo autorizan el código en línea que de verdad quieres publicar. Los navegadores ignoran 'unsafe-inline' en una directiva que también contiene un nonce o un hash (MDN), y por eso puede quedarse en una política estricta como reserva para navegadores muy antiguos.

Nonces

Un nonce es un valor aleatorio que el servidor genera en cada respuesta, incluye en la cabecera y copia en el atributo nonce de cada script que quiere ejecutar. Tiene que ser impredecible y nuevo en cada respuesta; web.dev recomienda al menos 128 bits de un generador criptográficamente seguro. En una aplicación Node.js:

import crypto from 'node:crypto';

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;
  res.setHeader('Content-Security-Policy',
    `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self'`);
  next();
});

La plantilla escribe después <script nonce="…" src="/app.js"></script> con el mismo valor. Pon el nonce allí donde tus plantillas generan tus propias etiquetas script, nunca reescribiendo todos los <script> del HTML terminado: una sustitución general también daría por bueno un script inyectado. Hay dos trampas frecuentes: un CDN o una caché de página que guarda el HTML repite el mismo nonce a todos los visitantes, y un valor fijado en un archivo estático durante la compilación no es un nonce.

Hashes

Un hash autoriza un único script en línea exacto: el resumen SHA-256, SHA-384 o SHA-512, codificado en base64, del texto que hay entre las etiquetas script. Cuenta cada carácter, espacios y saltos de línea incluidos, así que un cambio en el minificador o en la plantilla genera un hash distinto. Por eso los hashes encajan mejor en páginas estáticas o en caché, donde el contenido es igual para todos. Chrome muestra en la consola el hash que esperaba cuando bloquea un script en línea; también puedes calcularlo tú:

printf '%s' 'console.log("hello")' | openssl dgst -sha256 -binary | openssl base64

Lo que no permite ninguno de los dos

Los manejadores de eventos en línea como onclick="…" y las URL javascript: quedan bloqueados con una política basada en nonces o hashes. Lleva ese código a archivos de script y engánchalo con addEventListener. 'unsafe-hashes' puede autorizar un manejador concreto por su hash, pero úsalo solo como parche temporal. El código que ejecuta cadenas como código, por ejemplo eval(), new Function() o setTimeout con una cadena, necesita 'unsafe-eval'; sustitúyelo cuando puedas y recurre a 'wasm-unsafe-eval', más limitado, si solo hace falta compilar WebAssembly (MDN script-src).

4. strict-dynamic: que los scripts de confianza carguen lo que necesitan

Casi todos los sitios ejecutan scripts que cargan otros scripts: un gestor de etiquetas añade la analítica, un banner de consentimiento añade a sus proveedores, un chat trae su propio paquete. Enumerar todos los hosts que podrían usar es frágil. 'strict-dynamic' va por otro camino: un script autorizado por nonce o hash puede añadir más scripts, y esos también se consideran de confianza.

En los navegadores que lo admiten, 'strict-dynamic' hace además que se ignoren las listas de hosts, 'self' y 'unsafe-inline' dentro de script-src (MDN). Eso tiene tres consecuencias:

  • Cada etiqueta <script> de tu HTML necesita el nonce o un hash que coincida, también las de tu propio origen, porque 'self' ya no las autoriza.
  • Los scripts que un script de confianza crea con document.createElement('script') se permiten. Los que se escriben en la página con document.write (scripts insertados por el analizador) no, así que los fragmentos de inserción antiguos que dependen de ello dejan de funcionar.
  • La confianza se transmite. Todo lo que cargue tu gestor de etiquetas se ejecutará, de modo que quien pueda publicar en él puede, en la práctica, ejecutar código en tu web. Trata ese acceso como un acceso de despliegue.

Para navegadores antiguos, añade valores de reserva que los modernos ignoran:

script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' https: 'unsafe-inline'; object-src 'none'; base-uri 'none'

Un navegador compatible con CSP Level 3 aplica solo el nonce y 'strict-dynamic'. Uno que entiende los nonces pero no 'strict-dynamic' usa el nonce y https:, y uno muy antiguo se queda con 'unsafe-inline' https:. Google documenta un fragmento de Tag Manager compatible con nonces que transmite el nonce a los scripts que añade, y advierte de que las variables de JavaScript personalizadas de Tag Manager siguen necesitando 'unsafe-eval' (guía CSP de Tag Manager).

5. frame-ancestors, object-src 'none' y base-uri: directivas que debes escribir tú

  • frame-ancestors decide qué sitios pueden insertar tu página en un marco y es la defensa contra el clickjacking. Usa 'none' si nadie necesita enmarcarla, 'self' si solo lo hacen tus propias páginas, o enumera orígenes exactos como https://app.example.com. Solo funciona en la cabecera HTTP. X-Frame-Options (DENY o SAMEORIGIN) sigue siendo una reserva para navegadores antiguos; haz que ambas permitan lo mismo (MDN frame-ancestors).
  • object-src 'none' bloquea <object> y <embed>. Los plugins ya no existen en los navegadores modernos, pero estos elementos todavía pueden cargar contenido, y una política estricta sin default-src los deja libres si no lo indicas.
  • base-uri 'none', o 'self' si tus páginas usan un elemento <base>, impide que un <base href> inyectado redirija todas las URL relativas de scripts a otro host.
  • form-action 'self' limita adónde pueden enviarse los formularios. Añade el origen de tu proveedor de pago o de inicio de sesión si algún formulario envía datos allí, y revisa esos flujos después del cambio.
  • upgrade-insecure-requests hace que el navegador cargue por HTTPS los subrecursos http://. Ayuda con el contenido mixto, pero no sustituye a HSTS.

Escribe estas directivas de forma explícita en todas tus políticas, también en los ejemplos B y C. Apenas requieren mantenimiento y cierran huecos que las reglas de scripts dejan abiertos.

6. Informes de infracciones con report-to y report-uri

Los informes de infracciones cuentan qué bloqueó una política, o qué bloquearía en modo Report-Only, en los navegadores de visitantes reales. CSP Level 3 define report-to, que nombra un endpoint declarado en la cabecera Reporting-Endpoints, y marca como obsoleta la antigua report-uri, que recibe una URL directamente. MDN indica que report-to está disponible en los navegadores actuales desde 2026, mientras que las versiones antiguas solo entienden report-uri. Los navegadores que admiten report-to ignoran report-uri, así que de momento envía ambas (MDN report-to):

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: script-src 'nonce-R4nd0mBase64Value' 'strict-dynamic' 'report-sample'; object-src 'none'; base-uri 'none'; report-uri https://example.com/csp-reports; report-to csp

Los dos mecanismos envían formatos distintos. report-uri manda un objeto JSON con la clave csp-report y el tipo de contenido application/csp-report. report-to manda application/reports+json: una lista de informes cuyo type es csp-violation y cuyo cuerpo incluye campos como documentURL, blockedURL, effectiveDirective y disposition (enforce o report). Con 'report-sample', el informe de un código en línea bloqueado incluye sus primeros 40 caracteres. Acepta ambos formatos en el mismo endpoint.

  • Cuenta con ruido. Las extensiones del navegador inyectan scripts y provocan infracciones con archivos de origen como chrome-extension:// o moz-extension://. Agrupa los informes por directiva y host bloqueado, y busca patrones en lugar de entradas sueltas.
  • Trata los informes como datos personales. Las URL de documento y los referrers pueden llevar parámetros con direcciones de correo o tokens. Elimínalos o recórtalos, guarda los informes poco tiempo y no los compartas sin motivo.
  • Protege el endpoint. Cualquiera puede enviar informes falsos: aplica límites de tamaño y de frecuencia, y no muestres nunca su contenido sin escapar en un panel de administración.

7. Lo que suele romperse: analítica, fuentes, scripts en línea e incrustaciones

Casi todas las infracciones de los primeros días vienen de unas pocas fuentes. El campo effectiveDirective del informe te dice qué regla revisar:

SíntomaDirectiva en el informeSolución habitual
La analítica deja de registrar visitasscript-src-elem, connect-src, img-srcCargar la etiqueta con nonce, o permitir su host de script, y permitir los hosts de recogida que documenta el proveedor (Google los publica por producto)
Las fuentes web pasan a las del sistemafont-src, style-src-elemPermitir el host de la hoja de estilos en style-src y el de los archivos de fuente en font-src (con Google Fonts, fonts.googleapis.com y fonts.gstatic.com), o alojar las fuentes tú mismo
Un fragmento del tema o del CMS deja de funcionarscript-src-elem con blockedURL «inline»Añadir el nonce en la plantilla, mover el código a un archivo o autorizar su hash
Los botones con onclick no hacen nadascript-src-attrReescribir el manejador con addEventListener en un archivo de script
Las variables personalizadas del gestor de etiquetas no devuelven nadascript-src (eval)Sustituirlas por variables integradas o aceptar 'unsafe-eval' como excepción documentada
Un vídeo, mapa o marco de pago incrustado se queda en blancoframe-srcAñadir el origen exacto del proveedor
Se ignoran los estilos en línea y el diseño se descolocastyle-src-elem, style-src-attrMover los estilos a hojas de estilo, poner el nonce en los elementos style o mantener 'unsafe-inline' solo en style-src, como excepción documentada
El chat o las actualizaciones en vivo no conectanconnect-srcAñadir los hosts https:// y wss:// del proveedor
Tu aplicación o un socio ya no puede incrustar la páginaframe-ancestorsEnumerar los orígenes exactos que la incrustan

Los estilos en línea son un riesgo menor que los scripts en línea, pero no inofensivo: el CSS inyectado puede cambiar lo que ve el visitante y, con selectores de atributo, filtrar algunos datos de la página. Muchos frameworks y librerías CSS-in-JS insertan elementos style en tiempo de ejecución, así que comprueba si el tuyo admite nonces antes de quitar 'unsafe-inline' de style-src. Alojar tú mismo fuentes y librerías elimina varias de estas filas de una vez y mantiene la política corta.

8. Cómo desplegar una CSP por etapas

  1. Inventario. Anota los scripts, estilos, fuentes, marcos y destinos de conexión de tus páginas clave: inicio, acceso, cuenta, pago y páginas con widgets incrustados. El panel Network de DevTools y la configuración de tu gestor de etiquetas son las fuentes más rápidas.
  2. Report-Only. Envía el borrador como Content-Security-Policy-Report-Only con informes activados. No se bloquea nada; las infracciones aparecen en la consola y en tu endpoint. Déjalo el tiempo suficiente para que aparezcan las páginas menos visitadas y las campañas, normalmente una semana o más.
  3. Corrige el origen, no la política. Añade nonces, saca los manejadores en línea a archivos, aloja las fuentes en tu servidor. Añade un host o 'unsafe-eval' solo como excepción documentada, con responsable y motivo.
  4. Aplica. Cambia el nombre de la cabecera a Content-Security-Policy. Mantén a su lado una política Report-Only más estricta para ensayar el siguiente ajuste: cuando llegan ambas cabeceras, el navegador aplica una y solo informa sobre la otra.
  5. Sigue vigilando. Una etiqueta de marketing nueva, la actualización de un plugin o de un framework pueden traer código en línea nuevo. Mantén el endpoint de informes activo y vuelve a revisar la cabecera después de cada cambio de servidor, CDN o framework.

Dos detalles de infraestructura causan muchas sorpresas. Si un CDN o un framework añade su propia cabecera CSP, el navegador aplica todas las políticas que recibe y un recurso tiene que superarlas todas, de modo que una cabecera extra solo puede endurecer, nunca relajar (MDN). Y si el HTML se guarda en caché en el borde, una política con nonce necesita que el nonce se genere donde se monta la página, o pasarse a hashes. Para ver lo que una página envía de verdad:

curl -sS -D - -o /dev/null https://example.com/ | grep -i content-security-policy

La guía para comprobar las cabeceras de seguridad cubre las cabeceras que acompañan a la CSP, y la lista de seguridad para el lanzamiento de una web las integra en una revisión completa antes de publicar.

9. Revisa la cabecera publicada con el comprobador gratuito

Cuando la política ya se aplica, el comprobador gratuito de cabeceras de seguridad muestra lo que recibe un navegador desde tu página de inicio. Ejecuta el snapshot gratuito de preparación para el lanzamiento de Sitelemetry, ocho comprobaciones pasivas sobre un dominio público sin registro, y abre la comprobación de cabeceras por encima de las otras siete.

  • Solicita la página de inicio del origen que escribes, sigue hasta cinco redirecciones y lee la respuesta final. Otras páginas, como el acceso o el pago, suelen llevar políticas distintas; revísalas con DevTools o curl.
  • Solo lee la cabecera Content-Security-Policy aplicada. Durante la etapa Report-Only seguirá indicando que falta la CSP, con riesgo medio, porque una política de solo informe no bloquea nada. Es el resultado esperado hasta que la apliques.
  • Una directiva frame-ancestors en la política aplicada cuenta como protección de marcos, igual que X-Frame-Options.
  • En la nota de endurecimiento, que va aparte (de A a F), la CSP suma menos cuando default-src o una directiva script-src o style-src (incluidas sus variantes -elem y -attr) contiene 'unsafe-inline', o cuando default-src o una directiva de scripts contiene 'unsafe-eval'. La nota lee la política tal como está escrita, así que un 'unsafe-inline' de reserva junto a un nonce también resta, aunque los navegadores actuales lo ignoren.
  • No evalúa nonces, hashes, 'strict-dynamic', object-src, base-uri ni los informes. Para eso usa la consola del navegador y tus informes de infracciones.

El resultado muestra una puntuación y el resultado de cada una de las ocho comprobaciones, de modo que la señal de la CSP aparece junto a HSTS, TLS y el resto de comprobaciones públicas.

Preguntas frecuentes

¿Qué ejemplo de Content-Security-Policy es bueno para empezar?

Para un sitio renderizado en el servidor: script-src 'nonce-{aleatorio}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'self', con un nonce nuevo en cada respuesta. Para páginas estáticas o en caché, usa hashes de tus scripts en línea en lugar del nonce. Empieza con cualquiera de las dos como Content-Security-Policy-Report-Only.

¿Uso un nonce o un hash en mi CSP?

Usa un nonce cuando tu aplicación genera cada página, porque puede insertar un valor nuevo cada vez. Usa hashes cuando el HTML es estático o está en caché, porque un nonce en caché se repetiría para todos los visitantes. Ambos se combinan con 'strict-dynamic'.

¿Es un problema tener 'unsafe-inline' en style-src?

Es un riesgo menor que 'unsafe-inline' en script-src, pero el CSS inyectado puede cambiar lo que ven los visitantes y filtrar algunos datos de la página. Mantenlo solo como excepción documentada mientras pasas los estilos a hojas de estilo o añades nonces a los elementos style.

¿Content-Security-Policy-Report-Only protege mi web?

No. No bloquea nada; solo informa de lo que bloquearía una política aplicada. Úsala para ensayar y luego cambia a la cabecera Content-Security-Policy. Puedes enviar las dos a la vez, algo útil para probar la siguiente versión, más estricta.

¿Puedo definir la CSP en una etiqueta meta?

Sí, para la mayoría de directivas. Pero frame-ancestors, report-uri y sandbox se ignoran en una etiqueta meta, y una política Report-Only no puede entregarse así. Usa la cabecera HTTP siempre que tu servidor o tu hosting te lo permitan.

¿Qué diferencia hay entre report-uri y report-to?

report-uri recibe una URL directamente; CSP Level 3 la declara obsoleta, pero los navegadores antiguos todavía dependen de ella. report-to nombra un endpoint de la cabecera Reporting-Endpoints y usa el formato de la Reporting API. Los navegadores que admiten report-to ignoran report-uri, así que enviar ambas no causa problemas.

Fuentes y lecturas

  1. W3C: Content Security Policy Level 3www.w3.org
  2. MDN: guía de Content Security Policy (CSP)developer.mozilla.org
  3. MDN: cabecera Content-Security-Policydeveloper.mozilla.org
  4. MDN: directiva CSP script-srcdeveloper.mozilla.org
  5. MDN: directiva CSP frame-ancestorsdeveloper.mozilla.org
  6. MDN: directiva CSP report-todeveloper.mozilla.org
  7. MDN: cabecera Reporting-Endpointsdeveloper.mozilla.org
  8. W3C: Reporting APIwww.w3.org
  9. web.dev: Mitigate cross-site scripting (XSS) with a strict Content Security Policy (CSP)web.dev
  10. Google Tag Platform: Use Tag Manager with a Content Security Policydevelopers.google.com
  11. OWASP Content Security Policy Cheat Sheetcheatsheetseries.owasp.org
Equipo de Sitelemetry

Preparado por el equipo editorial de Sitelemetry. Consulta las fuentes enlazadas para profundizar.

SITELEMETRY

Pon en práctica lo aprendido.

Explora la estructura, configuración y señales de tu web con Sitelemetry.