Stored XSS and DOM-based XSS
Reflected XSS needs the victim to click a crafted link. Stored XSS needs nothing of the sort: the malicious content waits quietly in the application, and every visitor who opens the page receives it. DOM-based XSS, for its part, never even passes through the server. Understanding the three flavours lets you place any XSS you find into the right category.
Lab only
Stored XSS is particularly dangerous in a real application because it affects every user who views the infected content, including administrators. Never place test content in the comments, profiles or forums of real websites. In your lab, you can add harmless markers and clean up afterwards by resetting the application.
Stored XSS
In stored (also called persistent) XSS, the application saves input in a database or a file and later displays it to other users without encoding. Typical places:
- Comments, reviews and forum posts.
- User profile fields: display name, biography, avatar description.
- Support tickets and contact messages that are read by staff.
- Product names or descriptions edited by sellers.
- Log viewers and admin dashboards that display visitor data such as the browser identification header.
The last two are the most worrying: content submitted by an anonymous visitor ends up on a page viewed by a privileged employee. An attacker needs no victim to click anything; the application delivers the script to the target for them.
Think of a notice board in a shared office. If anyone can pin a note, and the office manager follows any instruction found on the board, a stranger gives orders to the manager. The notice board is the database; the manager is the browser executing the page.
Testing for stored XSS in a lab
- Identify storing features. From your inventory: anything that saves text and shows it again.
- Submit a unique marker with harmless markup (as in the previous lesson), for example a marker word with emphasis tags.
- Find every place it is displayed. The text you saved in a profile may appear on the profile page, on a list page, in an email, in an admin screen and in a PDF export. Each output has its own context and its own encoding, so one may be safe and another vulnerable.
- Check treatment at each place. Is the markup rendered or displayed literally?
- Record all points. Note who sees it: the author only, other users, or admins. This determines the severity.
In DVWA’s “XSS (Stored)” module (low level), the guestbook shows your entry to everyone. Use a marker and observe, then reset the guestbook with the application’s reset option so that your lab stays clean.
Severity thinking
Not every XSS is equal. Consider: who sees the content (anyone, logged-in users, administrators)? Does the page contain sensitive actions or data? Are cookies protected? A stored XSS on a page seen by administrators has a much higher impact than one only displayed to the author themselves (sometimes called “self-XSS”). These considerations will feed into the scoring in chapter 7.
DOM-based XSS
With the two previous kinds, the server writes the dangerous content into the HTML it sends. In DOM-based XSS, the dangerous step happens in the browser: the page’s own JavaScript reads data from a source that the user can influence, and writes it into the page using an unsafe method.
The DOM (Document Object Model) is the live, in-memory representation of the page that scripts can read and modify. The vocabulary testers use is:
- Source: where the data comes from. Examples: the URL query or the fragment after
#, the referrer, stored browser data, messages from other windows. - Sink: a function or property that interprets data as code or HTML. Examples:
innerHTML,document.write, and functions that evaluate strings as code.
Here is an intentionally vulnerable snippet for understanding:
// Source: the part of the URL after the # sign
const name = window.location.hash.substring(1);
// Sink: inserting it as HTML
document.getElementById('greeting').innerHTML = 'Welcome, ' + name;
The server never sees the fragment: browsers do not send it in requests. That means server logs and server-side filters know nothing about it, and the flaw can be invisible in the HTTP history. Testers find it by reading the page’s JavaScript and by observing how the DOM changes.
The safe version uses a sink that treats data as text:
const name = window.location.hash.substring(1);
document.getElementById('greeting').textContent = 'Welcome, ' + name;
textContent never interprets markup, so the same input is displayed literally. The MDN documentation at developer.mozilla.org describes these properties and their security implications clearly.
Testing for DOM-based XSS in a lab
- Open the browser’s developer tools and read the scripts that handle URL values, storage or messages.
- Search the code for the sink names listed above and trace back where their data comes from.
- Test with a harmless marker in the identified source, and watch the Elements panel: does your markup appear as real elements?
Juice Shop and the Web Security Academy have DOM-based XSS exercises, and the Academy explains source and sink analysis step by step. Because the logic is in JavaScript, the browser’s inspector is more useful here than the proxy.
Comparing the three types
| Reflected | Stored | DOM-based | |
|---|---|---|---|
| Where is the flaw? | Server response | Server response (from the database) | Client-side JavaScript |
| Does it need a victim to act? | Yes, usually a click | No, only to view the page | Yes, usually a crafted link |
| Persistent? | No | Yes | No (unless data is stored) |
| Visible in server traffic? | Yes | Yes, when saved and displayed | Possibly not |
| Typical fix | Output encoding | Output encoding | Safe sinks, avoid HTML insertion |
Rich text: a special difficulty
Some applications intentionally allow formatting (bold, links, lists) in comments or articles. Output encoding would break that feature. In such cases, the application should use a well-maintained HTML sanitisation library that parses the content and keeps only an allowlist of safe tags and attributes. Writing your own regular expression to “clean” HTML is a well-known source of bypasses. When testing, rich-text editors deserve extra attention for this reason.
Defences that fit all types
- Encode output for the exact context, and rely on the framework’s safe defaults.
- Prefer safe DOM APIs (
textContent) to HTML-inserting ones. - Sanitise any HTML that must be accepted, with a maintained library and an allowlist.
- Set session cookies as
HttpOnlyto limit what script can steal. - Add a Content Security Policy (lesson 4) to restrict what scripts can run, as a second line of defence.
Tip
For stored XSS, always check how the content looks in the admin interface. It is common for developers to encode output in the public site and forget it in the back office, where the impact is the highest.
Your turn: classify three scenarios
For each scenario, say which XSS type it is and who is most at risk: (1) a shop’s search page that repeats your query in its heading; (2) a customer review field whose text appears on the product page seen by all visitors; (3) a single-page application that reads a value after the # in the URL and inserts it into the page using innerHTML.
See a solution
- Reflected XSS. The query is echoed in the response; the victim must open a link containing it.
- Stored XSS. The review is saved, then served to every visitor, including staff who moderate reviews.
- DOM-based XSS. The source is the URL fragment, the sink is
innerHTML; the server never sees the value.
For each, write in your notebook one fix: output encoding for 1 and 2, and replacing innerHTML with textContent (or sanitising) for 3.
Key takeaways
- Stored XSS saves the payload in the application and hits every viewer, including privileged users, without needing a click.
- DOM-based XSS happens in client-side JavaScript from a source to a sink and may never appear in server traffic.
- Test every place where stored content is displayed; encoding may differ between the public site and the admin area.
- Use safe DOM sinks, contextual encoding and, for rich text, a maintained sanitisation library.
- Assess severity by who sees the content and what the page allows.
XSS makes the browser run attacker code. CSRF is subtler: it makes the browser do something the user never intended, using nothing but the user’s own logged-in session.