What Makes Xss Actually Different From Csrf
CSRF is a forged request arriving from outside. Cross-site scripting is the attacker's own code running inside your origin, which is why none of the defenses from the last chapter touch it.
The last post ended on a line I want to take seriously instead of rushing past: cross-site scripting stops asking the browser to send a forged request, and instead runs its own code inside your origin. That single difference changes everything about how this attack works, and why almost nothing from the last three chapters can stop it.
Outside the House vs. Inside the House
Every attack in the CSRF chapter shared one shape. The attacker's page lived somewhere else entirely, and the only thing that ever reached your site was a request, forged, unwanted, but still just a request arriving from outside.
Cross-site scripting throws that shape out completely. The attacker's code isn't knocking from outside anymore, it's already running inside your own page, in your own origin, with your own permissions.
That's why CORS, covered last post, does nothing here. CORS governs which other origins get to talk to yours. Once an attacker's script is executing as your origin, there's no "other origin" left to check against, because as far as the browser is concerned, it's just your code doing what your code is allowed to do.
Same-site cookie attributes fail for the identical reason. SameSite=Strict blocks a cookie from riding along on a request that originates from somewhere else. A script running on your own page was never "somewhere else" to begin with, so SameSite has nothing to enforce.
Why This Is Harder to Pull Off, and Worse When It Lands
Getting code to actually execute inside someone else's origin takes real work: finding an input that isn't sanitized, finding a way past whatever sanitization does exist, and getting that payload in front of a victim. That's a meaningfully higher bar than CSRF, which only needs a victim to load a page.
The payoff on the other side of that bar is why XSS gets taken so seriously despite being harder to land.
Once an attacker's code is running in your origin, it has everything your own JavaScript has. Any cookie not marked HttpOnly is fully readable through document.cookie, exactly the gap that attribute was built to close. It has the DOM, meaning it can read and rewrite anything on the page. And it can fire authenticated requests exactly the way your real code would, with the user's real session attached, no forgery required because nothing needs to be forged.
CSRF makes the browser lie on the attacker's behalf. XSS makes the attacker's code indistinguishable from your own.
Three Shapes This Takes
Cross-site scripting isn't one attack, it's a category, and the shape it takes changes both how dangerous it is and how hard it is to kill.
Stored XSS happens when malicious input gets saved somewhere, usually a database, and then rendered back out for every single visitor who loads that page afterward. It's the most dangerous of the three because of blast radius alone: one successful injection, and every person who views that page from then on runs the payload. It's also the easiest to permanently kill. Find the bad row, delete it, and the attack is gone for everyone who hasn't already been hit.
Reflected XSS works differently. The malicious input arrives through something like a query parameter or a header, and gets echoed straight into the page's response without ever touching a database. The attack only exists for as long as someone keeps clicking a crafted link. Stop the link from circulating, and the exploit path disappears, even though the underlying code flaw that made it possible is still sitting there unpatched.
DOM-based XSS is a third category worth knowing the name of. It happens entirely on the client, where JavaScript already running on the page reads attacker-controlled data and writes it somewhere unsafe, without the payload ever passing through a server response at all. It's a more subtle case, and this post focuses on stored and reflected, since those two cover the large majority of real-world incidents.
ExpandStored XSS saved once and served to every visitor, next to reflected XSS requiring a fresh crafted link per victim, with DOM-based XSS named as a third, smaller category.
How a Real One Actually Gets Built: The Samy Worm
The best way I found to actually understand stored XSS wasn't a definition, it was watching how a real one got built. The Samy Worm, MySpace, 2005, is the clearest worked example there is.
MySpace wasn't careless. Their sanitizer stripped <script> tags and a list of other known-bad patterns before saving profile content. A sanitizer having one hole doesn't mean it had no defenses, and this one blocked plenty. Samy Kamkar just found the one thing it didn't.
He discovered a raw newline character inside a spot that still got interpreted by the browser, one the sanitizer never accounted for. That alone wasn't enough to run arbitrary code, it was just the crack that let him start building.
From there, every blocked pattern got worked around individually, one at a time:
- Quotes were escaped, so he used
String.fromCharCodeto reconstruct a quote character without ever typing one directly innerHTMLwas blocked as a literal string, so he split it as"inne" + "rHTML", string concatenation the sanitizer's pattern match never caught
None of this was one clever trick landed in a single try. It was patient, iterative trial and error against MySpace's specific defenses, submit something, see what got stripped, adjust, submit again.
The finished payload did two things the instant someone merely viewed an infected profile: it forced their browser to add Samy as a friend, and it copied the exact same payload onto their own profile, so the next visitor to see it got infected the same way. That's what made it a worm and not just a one-off attack. Every victim's profile became the next delivery vehicle, automatically, with no further action from Samy required.
In under 20 hours, it had spread to over a million infected profiles, generating roughly a million friend requests along the way. One newline character a sanitizer missed, and patience, did the rest.
What This Sets Up
The Samy Worm wasn't a one-off curiosity. It's the first entry in a pattern that keeps showing up, against companies with real security teams, well past 2005.
The Essentials
- CSRF is a forged request from outside. XSS is the attacker's own code executing inside your origin. That's why origin-based defenses like CORS and
SameSitedon't apply to XSS at all. - Once code runs in your origin, it has everything your JavaScript has, including any cookie not marked
HttpOnly, the full DOM, and the ability to fire authenticated requests exactly like your real code. - Stored XSS persists in a database and hits every visitor, which makes it the most dangerous but also the easiest to permanently kill by deleting the bad row.
- Reflected XSS lives in a crafted link, not a database. Its exploit path disappears the moment that link stops circulating, even though the code flaw remains.
- DOM-based XSS is a third, more subtle category, worth knowing exists even without a deep dive here.
- The Samy Worm shows these attacks are rarely a single trick. MySpace's sanitizer blocked plenty; Samy Kamkar spent real time finding and working around the one gap it missed.
Next up: the named incidents that followed Samy, and what they actually cost the companies that shipped them.
Further Reading and Watching
- Cracking Websites with Cross Site Scripting - Computerphile: a hands-on walkthrough of how an unsanitized input turns into running JavaScript.
- Cross Site Scripting (XSS): OWASP's reference entry covering the stored, reflected, and DOM-based categories in more depth.
Keep reading