Stored XSS
Plant an XSS payload in a comment, watch it fire for the next visitor, then ship the HTML-escaping fix.
What Is Stored XSS?
Stored XSS sits at the top of the cross-site scripting risk family because the payload only has to be planted once. Every subsequent user who loads the page downloads the attacker's script and runs it against their own session, no clicked link, no crafted URL, no awareness that anything is wrong. This exercise puts you on both sides of a real exploit. As Bob, a malicious actor hunting community forums, you find a guestbook that renders posted notes as raw HTML, drop the textbook image-with-onerror payload, and watch it execute on the posted wall as proof that it will run for every member who loads the page. As Alice, Signal Coast's application security engineer, you open the community wall yourself one morning and get hit on load (a popup printing your own session cookie fires the instant the guestbook renders), open the vulnerable render function in the code editor, and see the note body concatenated raw into the HTML response. You apply the escaping fix so every user-controlled field passes through an HTML escaper at the rendering boundary, then prove it by reloading the same wall with the attacker's payload still stored: before the fix the script runs, after the fix the same payload renders as inert text. The exercise closes with a quiz that tests whether you can distinguish stored from reflected XSS, explain why image tags fire even without script tags, and identify the right place in the request-response cycle to neutralize the payload (output encoding at the render boundary, not input filtering at submit).
What You'll Learn in Stored XSS
- Distinguish Stored XSS from Reflected XSS, the durable persistence of the payload on the server is what changes the threat model
- Explain why an <img src=x onerror=alert(...)> tag fires even when there is no <script> tag in the payload
- Identify the rendering-boundary HTML escape as the correct primary defense, and explain why input-layer blocklists and CSP-only strategies fall short
- Read a Node.js render function well enough to spot a concat-into-HTML bug and recognize the line where the escape needs to be inserted
- Understand that the stored value can stay exactly as-is once the renderer escapes properly, the database does not need to be sanitized for the fix to work
Stored XSS — Training Steps
-
Find the community forum
Today Bob is targeting Signal Coast, whose community forum lets any member post a short public note that every other member reads. He signs in with a throwaway account and opens the forum's guestbook to see how it handles what people write.
-
Notes render as written
The guestbook makes a promise most sites are careful never to make: it renders each note exactly as written, HTML included. A field that turns whatever you type into live markup is exactly what Bob was hoping to find.
-
Post a normal note
First, Bob behaves like any other member and posts an ordinary note to see how the guestbook handles it. The note joins the wall exactly as written: plain text, nothing unusual. This is the feature working as intended.
-
Escalate to a script
The field renders HTML, so Bob feeds it a script instead of a sentence. His probe is an image tag with a source that cannot load and an onerror handler that reads the viewer's session cookie. The image fails, the handler runs, and the browser pops up whatever the note tells it to. Here it prints the session, proof that a note can run code with the viewer's login attached.
-
It runs with the session
There it is: the note ran on its own and popped the viewer's live session cookie, with no link clicked and no file opened. A harmless print today; a silent beacon to Bob's server tomorrow. Bob has proven the wall runs whatever a note contains. Now he builds the real weapon.
-
Plant the fake login
Bob swaps the probe for a note that rewrites the page into a Session expired sign-in box, with a handler that beacons whatever is typed to his own server. To every member who loads the wall it looks like the forum simply asked them to sign in again. The credentials go straight to Bob.
-
The trap is armed
The fake login is now the newest note on the wall, and it looks just like the forum asking members to sign in again. Its handler beacons whatever is typed into it straight to Bob's server. He planted it once; every member who trusts it and signs in hands over their password.
-
Alice opens the wall
You own the guestbook's render code. This morning you catch up on the community, the same as any other admin starting their day: open your browser and go to the wall. You did not click a link or run anything. You are just loading a page you manage.
-
Hit on load
The page had barely finished loading and a popup is already printing your session cookie. You typed nothing and clicked nothing. One of Bob's notes ran the instant the wall rendered, inside your admin session. On his build the same handler would beacon that session straight to him.
-
And a fake login below it
Scroll the wall and there is a second planted note: a Session expired sign-in box that looks like the forum's own. Any member who trusts it and signs in hands their password to the attacker. These are stored notes, run by every viewer's browser. As the engineer who owns this page, you know the fix is in the render code.