Injection
Log in Join

Lesson 6 · Reading · 30 min

Stored XSS

Reflected XSS needs a victim to click your link. Stored XSS does not. Your payload is saved on the server and served to everyone who views the page, so a single injected comment can run in every visitor's browser, including staff. This is why stored XSS is usually the highest-severity XSS and one of the most serious client-side findings there is.

The shape

Stored (also called persistent) XSS happens wherever the application saves your input and later displays it to others. You post something, the server stores it, and every time anyone loads the page that shows it, your markup is served into their browser. If that markup contains a script and the app did not encode it on the way out, the script runs for each viewer.

The high-value places are anywhere user content is shown to other users:

  • Comments, reviews, forum and discussion posts: seen by everyone who reads the thread.
  • Profile fields: name, bio, "about", a website field, seen by anyone who views your profile, including admins moderating it.
  • Messages and support tickets: seen by the recipient or the support agent, who is often a privileged user.
  • Anything an admin reviews: a report, a submitted listing, a contact form landing in an admin dashboard. Firing script in an admin's session is often the whole game.
  • Filenames, order notes, any field that echoes into a later page.

Finding it

The method mirrors reflected XSS, but across two steps, inject on one page, look for execution on another:

  1. Submit a marker into every stored field: a unique string, then a probe with HTML characters, e.g. zqx<b>test</b>.
  2. View the page that displays it: and every other page that might, because stored input often resurfaces somewhere unexpected (a comment shows on the post page, in a "recent activity" feed, and in the admin queue). Check whether your <b> rendered as bold (markup interpreted, injectable) or appeared as literal <b> text (encoded, safe).
  3. If markup renders, escalate to a script payload appropriate to the context, exactly as in the reflected lesson: <img src=x onerror=alert(document.domain)> is a reliable proof that survives many CSPs better than a bare <script>.

The subtlety is where it lands and who sees it. A payload in your display name might be encoded on your own profile but rendered raw in the admin's user-management table. Always ask: which other page shows this value, and to whom?

Impact: it reaches people you never contacted

Because it is served to every viewer automatically, stored XSS reaches victims with no interaction. Practical impact, all running in the victim's session on the site's origin:

  • Session or account takeover: if the session cookie lacks HttpOnly, document.cookie hands it to the attacker; even with HttpOnly, the script can act as the victim (change their email, add an admin, perform any authenticated request).
  • Privilege escalation via an admin viewer: a stored payload that an administrator loads runs with the admin's session; it can create accounts, change roles, or plant a persistent backdoor. A stored XSS that reaches an admin panel is frequently critical.
  • Worming: a payload that, when it runs in a viewer's session, posts itself again spreads across users. (Prove the concept; never actually release one.)

Encoding, sanitisation, and their bypasses

Well-built apps defend stored content two ways: output encoding (turn < into &lt; when displaying, so it shows as text) and, for fields that must allow some HTML (a rich-text bio), sanitisation with an allow-list library. Sanitisers are where the interesting bugs are: they try to strip dangerous tags and attributes, and they are bypassed by malformed markup, unusual event handlers, javascript: URLs in links, SVG payloads, or mutation quirks where the browser "fixes" your broken HTML into something executable. If a field allows any formatting, probe what it lets through, an allowed <a href> that accepts javascript:, an allowed <img> that accepts onerror.

The honesty gate

Stored XSS is powerful and persistent, which makes careless testing genuinely harmful: a payload you save stays live for real users. So on a real target:

  • Use a benign proof: alert(document.domain) or a call to a logger you control that only records "it fired", never a real cookie-stealer harvesting real sessions.
  • Clean up: delete the payload after proving it, so you do not leave a live XSS hitting real users while triage is pending.
  • Scope it to yourself where possible, a stored payload on your own profile that you then view as your own second account proves persistence without touching strangers.

On this Academy's labs, none of this touches real people, so experiment fully.

Your labs

CampusConnect discussion board stored XSS is the classic case: post to a board, and your markup is served to every reader. TicketDesk support (stored XSS) is the high-value variant: your input lands in a support agent's view, so execution happens in a privileged session, the strongest impact story XSS has. Do the board first for the mechanism, then the ticket to see why "who views it" changes the severity.

Where this goes next

Reflected and stored XSS both pass through the server. The last XSS lesson is the one that never does: DOM-based XSS, a bug that lives entirely in the page's own JavaScript, invisible if you only read the server's response.

This lesson has 3 labs to try what you just read.