JavaScript › JavaScript Security Deep Dive
The cross-site scripting (XSS) family
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 <). 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?
All three share the root cause that attacker-controlled input ends up being treated as code (HTML or JavaScript) in a victim's browser. They differ only in where the input travels — reflected in the request, stored in the database, DOM-based purely in the client. The one principle that prevents all of them is to treat untrusted input as data, never as code: escape output, use safe APIs like textContent, and sanitise when HTML must be allowed.
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.