The Three Types

Chapter 2
The Three Types: Reflected, Stored, DOM-Based
How each delivers the payload — and the server-side vs client-side split

All XSS shares the same root cause (Chapter 1: input treated as code), but it's classified by where the malicious input comes from and how it reaches the page. The three types — Reflected, Stored, and DOM-based — differ in delivery, persistence, and which part of the stack fails. Knowing the type tells you where to look and how to fix it.

Type 1: Reflected XSS

The payload is included in a request (typically a URL parameter), and the server immediately reflects it back in the response without encoding. It isn't stored — it only affects whoever makes that specific request, so the attacker must deliver the crafted URL to the victim (a link in an email, a message, a malicious ad).

// server reflects the search term straight into the page // https://site.com/search?q=shoes -> "You searched for: shoes" res.send(`<p>You searched for: ${req.query.q}</p>`); // unescaped! // attacker sends victim this link: // https://site.com/search?q=<script>steal()</script>

When the victim clicks the link, the server echoes the script into the response and the victim's browser runs it. Reflected XSS is the most common type and requires the victim to interact with an attacker-supplied request.

Type 2: Stored XSS

The payload is saved on the server (in a database, comment, profile field, log) and later served to every user who views the affected page — no special link required. This makes it the most dangerous type: one injection can hit thousands of victims automatically.

// attacker submits a comment containing a script comment = "<script>steal()</script>"; // saved to the DB // later, the comments page renders every comment unescaped: comments.forEach(c => html += `<li>${c.text}</li>`); // runs for every viewer

Every visitor to the comments page executes the attacker's script in their own session, with no action beyond visiting a normal page. Stored XSS on a high-traffic page is how XSS worms spread (Chapter 5).

Type 3: DOM-Based XSS

The vulnerability is entirely client-side: the page's own JavaScript takes attacker-controllable input (from the URL, location.hash, etc.) and writes it into the page through a dangerous "sink" — the malicious data never needs to touch the server.

// the page's own JS reads the URL fragment and injects it const name = location.hash.slice(1); document.getElementById("greet").innerHTML = name; // dangerous sink // https://site.com/#<img src=x onerror=steal()> -> runs, server never sees it
DOM-based XSS can be invisible to the server — and to server-side defences
Because the payload flows from a client-side source (like location.hash, which browsers don't even send to the server) to a client-side sink (innerHTML, eval, document.write), the malicious data may never reach the backend. That means server-side output encoding — the primary defence for reflected/stored — does nothing here. DOM XSS must be fixed in the client JavaScript itself (safe sinks like textContent, Chapter 5). It's also harder to spot in testing, since server logs show nothing unusual.

The Key Split: Server-Side vs Client-Side

ReflectedStoredDOM-based
Payload sourcethe request (URL/form)server storage (DB)client-side (URL/hash/etc.)
Where the flaw isserver reflects unescapedserver stores & serves unescapedclient JS writes to a sink
Persistent?No — per requestYes — affects all viewersNo — per request
Needs victim to click a link?YesNo — just view the pageUsually yes (crafted URL)
Reaches the server?YesYesOften No
Fixed by server-side encoding?YesYesNo — fix the client JS
Two questions classify any XSS instantly
(1) Is the payload stored and served to others, or reflected from the immediate request? Stored → Stored XSS; reflected → Reflected or DOM. (2) Does the injection happen in server-rendered HTML, or in client-side JavaScript writing to the DOM? Server → Reflected/Stored; client-only → DOM-based. Reflected and DOM-based can look similar (both per-request, often via URL) — the distinguishing question is who does the unsafe insertion: the server (reflected) or the page's own JS (DOM).

Severity, Roughly

  • Stored — generally highest: persistent, hits all viewers automatically, no per-victim delivery, can self-propagate (worms).
  • Reflected — common and serious, but needs the victim to follow an attacker's link, limiting reach per attack.
  • DOM-based — severity varies, but dangerous because it bypasses server-side defences and is easy to miss; can be reflected-like or stored-like depending on the source.

These are tendencies, not rules — a reflected XSS on a bank's login page can be more damaging than a stored XSS in an obscure admin note. The next three chapters take each path deeper, starting with the injection contexts that determine how a payload must be shaped.

Hands-On Exercises

Exercise 1

For each scenario, classify the XSS type and justify it: (a) a script in a ?q= URL parameter echoed into search results; (b) a script saved as a product review and shown to all shoppers; (c) the page's JS reading location.hash into innerHTML. State for each whether the payload reaches the server.

📄 View solution
Exercise 2

Explain why server-side output encoding fixes reflected and stored XSS but does nothing for DOM-based XSS. Identify what part of the stack must change to fix a DOM-based bug, and why it can be invisible in server logs.

📄 View solution
Exercise 3

Argue why stored XSS is usually rated more severe than reflected XSS, covering persistence, victim reach, and delivery requirements. Then give one realistic case where a reflected XSS would be more damaging than a particular stored XSS.

📄 View solution

Chapter 2 Quick Reference

  • Reflected — payload in the request, echoed back unescaped by the server; not stored; needs the victim to click an attacker's link
  • Stored — payload saved server-side, served to every viewer; most dangerous; can self-propagate (worms)
  • DOM-based — client-side JS reads attacker input (URL/hash) into a dangerous sink; often never reaches the server
  • Server-side encoding fixes reflected & stored, but does nothing for DOM-based — fix the client JS (safe sinks)
  • Classify with two questions: stored vs reflected? and server-rendered vs client-JS insertion?
  • Reflected vs DOM look similar (per-request, URL-driven) — distinguish by who inserts unsafely: server vs page's own JS
  • Severity (rough): Stored ≥ Reflected ≥/≈ DOM, but context can flip it
  • Next chapter: injection contexts — HTML, attribute, JS, URL, CSS — and why each needs different handling