Stored XSS vs Reflected XSS vs DOM XSS

The three cross-site scripting classes, separated by where the payload lives between injection and execution — and why the taxonomy is messier than three boxes.

The short answer

All three end the same way: attacker-controlled script executing in a victim's browser under your origin. They differ in the route the payload takes to get there:

One distinction is worth holding onto before the details, because it explains most of the confusion around these terms: stored and reflected describe what the server does, while DOM-based describes where the bug is. Those are two different axes, not three slots on one.

StoredReflectedDOM-based
Where the payload livesServer-side storageThe request itselfThe browser only
Does the server see itYes — and keeps itYes — and echoes itNo
Where the flaw isServer-side renderingServer-side renderingClient-side JavaScript
Victim action neededVisit a normal pageClick a crafted linkClick a crafted link
Blast radiusEvery viewer, repeatedlyOne victim per clickOne victim per click
Typical sinkTemplate outputTemplate outputinnerHTML, eval, document.write
How it is foundMarker injection, then crawlParameter fuzzingReading JS or browser taint tracking
Visible to a WAFOn the way inOn the way in or outNever
Visible in server logsYesYesNo
Server-side encoding fixes itYesYesNo

Stored XSS

Input is saved — a comment, a display name, a support ticket, a filename — and later rendered into a page without encoding appropriate to its context. Every visitor to that page executes it, with their own session.

It is the most severe of the three for a structural reason: it needs no delivery mechanism. There is no link to distribute and no victim to convince, so the whole social-engineering step that caps reflected XSS simply does not apply. And the impact scales with who visits: the same flaw in a user-facing comment field is serious, while in an admin dashboard it is privilege escalation, because the next person to load the page is an administrator.

The variant worth knowing is blind stored XSS, where the payload lands somewhere the attacker cannot observe — an internal log viewer, a moderation queue, a generated report. You get no reflected response to confirm anything, so detection depends on out-of-band signalling: the payload calls home, and the callback tells you it fired and where. Any input that eventually reaches a human reviewing it is a candidate, and those paths are rarely covered by tests.

Finding it systematically means submitting uniquely identifiable markers into every field you can write to, then crawling the whole application looking for those markers rendered in an executable context — a very different exercise from fuzzing one parameter at a time.

Reflected XSS

A parameter from the request is written into the response without encoding — the classic case being a search page that prints No results for <your query>. The server never stores anything; the payload exists only for the lifetime of that request.

Because exploitation requires the victim to follow an attacker-supplied link, reflected XSS is often triaged down. That underrates it in two situations. First, when the vulnerable page is authenticated: the victim is already logged in when the script runs, so the payload acts with their session immediately. Second, when it chains — reflected XSS plus a state-changing endpoint that the injected script can call defeats CSRF token protection entirely, because the script reads the token from the page. The severity of a reflected finding usually rests on what is reachable from the injected context, not on the injection itself.

Of the three this is the easiest to automate. Canary strings through every parameter, then check the response for unencoded reflection, is the approach every scanner implements.

DOM-based XSS

Here the server response is completely clean. The vulnerability is in JavaScript that reads from a source the attacker controls and passes it, unsanitized, to a sink that executes or parses markup.

Common sources: location.hash, location.search, document.referrer, window.name, postMessage payloads, and values previously written to localStorage. Common sinks: innerHTML, outerHTML, document.write, eval, the Function constructor, setTimeout called with a string, jQuery's .html(), and assigning a javascript: URL to href or src.

The single fact that makes DOM XSS different from the other two is this: a URL fragment is never sent to the server. Everything after the # stays in the browser. So a payload delivered through the fragment is invisible to your WAF, absent from your access logs, and unreachable by any server-side sanitization you might add. Defences that work for stored and reflected XSS do not merely underperform here — they never see the input.

That also makes it the hardest to find. There is no response to diff, so detection means reading the JavaScript — often minified and bundled — and tracing data flow, or using browser-side taint tracking (Burp's DOM Invader, or breakpoints on the sinks in DevTools). In bug bounty this is where competition is thinnest, for exactly that reason.

The taxonomy overlaps, and the queries assume it doesn't

Because stored and reflected classify server behaviour while DOM-based classifies the location of the sink, the three are not mutually exclusive. Stored DOM XSS is entirely ordinary: a value is persisted server-side, returned later by an API call as JSON, and then passed to innerHTML by client-side code. It is stored by persistence and DOM-based by mechanism, and treating the categories as three sealed boxes leads teams to fix it in the wrong place — adding server-side encoding to data that is never rendered server-side.

This is why OWASP's material now leads with a two-axis framing — server XSS versus client XSS, crossed with persisted versus reflected — rather than the familiar three types. The three-way split remains the common vocabulary, and it is what people search for, but the question that determines the fix is narrower: in which execution context does the untrusted value become code?

The defences are not interchangeable

Which to look for first

If you are hunting, the honest ordering is by effort rather than payout. Reflected XSS is the most automatable and therefore the most picked-over. Stored XSS pays better and rewards patience — inject markers broadly, then go looking for where they surfaced, including the places you were not meant to see. DOM XSS has the lowest competition precisely because it requires reading JavaScript that nobody else wants to read, and it is invisible to the scanners everyone else is running.

If you are defending, reverse it. Audit the DOM sinks first, because they are the ones your WAF and your server-side controls cannot see at all.

More comparisons: SSRF vs CSRF XSS vs CSRF AuthN vs AuthZ IDOR vs BOLA SQLi vs NoSQLi SAST vs DAST Bounty vs Pentest SBOM vs SLSA Validation vs Encoding DAST vs IAST vs RASP SCA vs SAST OAuth vs SAML Pentest vs Red Team WAF vs RASP