JavaScript › JavaScript Security Deep Dive

The cross-site scripting (XSS) family

6 min read Advanced 7 sections

Cross-site scripting is the most common web vulnerability, and it comes in three flavours. They share one root cause: attacker-controlled input ends up executing as code in a victim’s browser. Once you understand the source-to-sink model from the DOM lesson, all three kinds of XSS become variations on a single idea — and so do their fixes.

You'll learn to

  • Tell reflected, stored, and DOM-based XSS apart
  • Trace the source-to-sink flow in each
  • Know the one principle that prevents all three

The three kinds

Reflected XSS   input comes in via a request and is echoed straight back
                in the response, unescaped. The payload lives in a crafted link.

Stored XSS      input is saved (a comment, a profile field) and served to
                everyone who views it later. The payload lives in the database.

DOM-based XSS   input never reaches the server — client-side JavaScript reads it
                (from the URL, say) and writes it to a dangerous sink. Pure client.

The difference is where the input travels, but the mechanism is identical: untrusted data reaches a place that treats it as HTML or script.

Reflected XSS

Vulnerable page echoes the search term straight into the HTML:
  https://site.com/search?q=hello   →   "You searched for: hello"

The attack — a crafted link sent to a victim:
  https://site.com/search?q=<script>steal(document.cookie)</script>
  → the script is reflected into the page and runs in the victim's browser

The payload is in the URL; the victim has to click the attacker’s link. The server reflects the unescaped input back, and it executes.

Stored XSS

Attacker posts a comment containing a script payload.
The site saves it. Every user who later views that comment
has the script run in their browser — no clicking required.

Stored XSS is more dangerous because it’s persistent and self-spreading: one injected payload hits every viewer. A stored XSS in a high-traffic field (a profile name shown to admins, say) can be devastating.

DOM-based XSS

This is the pure-JavaScript kind from the DOM lesson — input flows from a client-side source (like the URL hash) to a dangerous sink (like innerHTML) without ever touching the server.

// Vulnerable: reads the URL fragment, writes it as HTML
document.getElementById("out").innerHTML = location.hash.slice(1);

The one principle that prevents all of them

Treat all untrusted input as DATA, never as code.

Every XSS fix is a version of this. Escape output so input renders as text, not HTML (< becomes &lt;). Use safe APIs (textContent, not innerHTML). Sanitise with a vetted library (DOMPurify) when you genuinely must allow some HTML. Set a Content-Security-Policy as defence in depth.

Checkpoint

What single root cause do reflected, stored, and DOM-based XSS all share, and what's the one principle that prevents all three?

Try it yourself

On a deliberately vulnerable practice app you control (such as a local DVWA or a CTF lab), identify which kind of XSS a given input field is — does the payload come back in the same response (reflected), persist for later viewers (stored), or get handled entirely by client-side JavaScript (DOM)? Trace the source and the sink in each case.

Summary

XSS has three forms — reflected (echoed from a request, delivered via a crafted link), stored (saved and served to every viewer, persistent and dangerous), and DOM-based (pure client-side, source to sink without the server). All share one root cause: untrusted input treated as code in a victim’s browser. The universal hunting method is source-to-sink tracing; the universal fix is treating input as data — escaping output, using safe APIs, sanitising when needed, and adding CSP as defence in depth.

Key takeaways

  • Reflected (in the request), stored (in the database), DOM-based (pure client).
  • All share one cause: untrusted input executing as code in the browser.
  • Hunt by tracing source to sink — the same method for all three.
  • Prevent by treating input as data: escape output, use textContent, sanitise, add CSP.

Quick quiz

Next in the deep dive, prototype pollution — a JavaScript-specific bug class where polluting a shared object cascades across the whole application.

Was this lesson helpful?