VVenstap
Application Security

Cross-Site Scripting (XSS) Explained

Dana Whitfield·

Cross-site scripting gets talked about as a single vulnerability, but it's really three related failure modes that share an outcome — an attacker gets arbitrary JavaScript to execute in a victim's browser session — while differing completely in cause, delivery mechanism, and remediation. Treating XSS as one thing is how teams end up fixing the flavor they understand and leaving the other two exposed.

The three flavors

Reflected XSS occurs when user input is echoed back in the immediate server response without proper encoding — a search page that renders "No results for <query>" with the raw query string inserted into the HTML. It requires the victim to click a crafted link, so it's typically delivered via phishing, but it's also the easiest for scanners to detect because the payload and its reflection happen in a single request-response cycle.

Stored XSS happens when malicious input is saved server-side — in a comment, a user profile field, a support ticket description — and then rendered unencoded to other users later. It's more dangerous than reflected XSS because it doesn't require tricking anyone into clicking a link; any user who views the affected page is compromised, including, in the worst cases, administrators viewing user-submitted content in an admin panel.

DOM-based XSS is the flavor most teams under-test for. The vulnerable code never touches the server at all — client-side JavaScript reads data from a source like location.hash, document.referrer, or a postMessage payload, and writes it into the DOM via a sink like innerHTML or document.write without sanitization. Because the payload never appears in server logs or server-rendered HTML, traditional server-side scanning and even manual review of backend code will completely miss it — you have to inspect the client-side JavaScript and trace data flow from source to sink.

Why encoding context matters more than "sanitizing input"

A common misconception is that XSS is fixed by "sanitizing input" at the point it enters the system. That's necessary but not sufficient, because the correct escaping rules depend entirely on where the data ends up being rendered, not where it came from. A string that's safely escaped for placement inside an HTML text node can still be dangerous if it's later placed inside an HTML attribute, inside a <script> block, inside a CSS value, or inside a URL — each of those contexts has different characters that need escaping and different injection techniques.

This is why output encoding at the point of rendering, matched to the specific context, is the more reliable model than trying to cleanse input once at the boundary. Modern frontend frameworks — React, Vue, Angular — auto-escape values bound into JSX or templates by default, which has eliminated a huge fraction of historical XSS. But every framework provides an opt-out for cases the default escaping can't handle: dangerouslySetInnerHTML in React, v-html in Vue, [innerHTML] bindings in Angular. Every one of these is a place where the framework's safety net is explicitly disabled, and every one of them should be treated as a flagged, reviewed exception, not a routine tool.

Practical defenses beyond encoding

  • Content Security Policy (CSP) as defense in depth — a well-configured CSP that disallows inline scripts and unsafe-eval significantly limits what an attacker can do even if an XSS injection point exists, though it should never be the only control.
  • HttpOnly and Secure cookie flags on session cookies prevent JavaScript — including injected JavaScript — from reading them directly, limiting the blast radius of a successful XSS to actions within the page rather than full session token theft.
  • Sanitization libraries for rich-text input — if users need to submit formatted content (a comment editor, a description field with bold/italic), use a well-maintained sanitizer like DOMPurify configured with an explicit allowlist, rather than a custom regex-based filter, which will almost always have bypasses.
  • Trace every dangerouslySetInnerHTML, v-html, and innerHTML assignment in the codebase as a standing review item, and confirm the data reaching it is either fully static or passed through a vetted sanitizer.
  • Test DOM-based XSS specifically by reviewing client-side JavaScript for source-to-sink data flow, since black-box scanning of server responses won't surface it.

XSS testing benefits enormously from pairing automated crawling with targeted manual review of client-side code, because the three flavors have genuinely different detection profiles — a scanner tuned for reflected and stored XSS via HTTP responses will not find DOM-based issues buried in a bundled JavaScript file. Venstap's finding triage supports exactly this kind of split: automated nuclei-based scan results and manually logged pentest findings land in the same queue, tagged by XSS subtype, so a team can see at a glance which flavor of the vulnerability class is actually covered and which still needs a dedicated code-level pass.

#xss#client-side-security#output-encoding

Ready to see Venstap in action?

Get a guided walkthrough of scanning, triage, and reporting on your own assets.