Vulnerability guide

XSS vulnerability

Learn what cross-site scripting is, how XSS vulnerabilities work, where reflected, stored, and DOM-based XSS appear, and how to prevent them with output encoding, sanitization, safe DOM APIs, and defense-in-depth controls.

Web application securityBrowser securityCWE-79Prevention guide

What XSS is

Cross-site scripting, usually shortened to XSS, is a browser-side injection vulnerability. It happens when an application places untrusted data into a web page in a way that the browser can treat as active content instead of plain text.

The dangerous part is the trust boundary. The victim's browser is already allowed to run code that belongs to the trusted site. If attacker-controlled content is allowed to enter that trusted page context, it may read page data, change the interface, submit requests, or mislead the user while appearing to come from the real application.

XSS is not only a JavaScript problem. The root issue is unsafe output in browser-parsed contexts: HTML, attributes, URLs, CSS, inline scripts, rich text, markdown previews, client-side templates, and DOM updates.

How XSS works

XSS usually starts with a value that the application receives, stores, or reads from the browser: a search term, comment, profile field, URL fragment, support ticket, API response, or markdown document. The bug appears later when that value is inserted into a page through a dangerous rendering path.

Modern frameworks reduce many common mistakes through escaping and interpolation, but escape hatches still exist. Direct DOM writes, unsafe rich HTML rendering, untrusted URLs, outdated plugins, and custom template logic can all turn data into executable browser content.

A safe pattern keeps untrusted content in text-only sinks unless the product intentionally supports HTML. When rich HTML is required, sanitize it with a maintained allow-list sanitizer and avoid modifying the sanitized result in a way that can reintroduce unsafe markup.

Unsafe rendering pattern
const profileName = request.query.name
document.querySelector('#profile-name').innerHTML = profileName

This pattern treats a user-controlled display name as markup. If the value contains browser-parsed content, the page may execute behavior that the application never intended.

Safer text rendering pattern
const profileName = request.query.name
document.querySelector('#profile-name').textContent = profileName

This pattern writes the same value as text. The browser displays the characters to the user instead of interpreting them as HTML, script, or event-handler markup.

Why XSS matters

XSS can turn a small display bug into account, data, and trust impact because it runs in the user's browser inside a real application session. The exact severity depends on what the affected page can access, which actions the user can perform, and which browser-side defenses are in place.

Even when session cookies are protected with HttpOnly, XSS can still interact with the page as the current user, read visible data, change forms, trigger requests, or guide the user through convincing phishing flows.

Account abuse

Injected browser-side code can perform actions that the current user is allowed to perform, such as changing settings, sending messages, or approving workflow steps.

Sensitive data exposure

Pages may expose tokens, personal data, invoices, messages, internal notes, CSRF tokens, or API responses to malicious browser-side logic.

Phishing and deception

A vulnerable trusted page can be modified to show fake login prompts, misleading payment instructions, hostile links, or support messages.

Session and token risk

Weak cookie settings, browser storage tokens, or exposed credentials can make XSS a direct path to session theft or long-lived account compromise.

Compliance and reputation impact

XSS affecting customer data, admin consoles, or regulated workflows can create disclosure obligations, audit findings, and visible loss of trust.

Common forms of XSS

XSS is usually grouped by where the unsafe data comes from and when the browser executes it. The practical lesson is the same across all forms: every untrusted value must be handled according to the browser context where it is rendered.

FormWhat to understand
Reflected XSSThe application immediately reflects request data, such as a search term or URL parameter, into a response without context-safe encoding.
Stored XSSThe application saves unsafe content, such as a comment, profile field, ticket, or document, and later serves it to other users.
DOM-based XSSClient-side JavaScript reads data from the URL, storage, postMessage, or an API response and writes it into a dangerous DOM sink.
Mutation XSSMarkup that looks harmless is changed by browser parsing or a library mutation into a shape that becomes unsafe.
Self-XSSA social-engineering pattern where a user is tricked into running code in their own browser; it is usually different from an application XSS flaw but can still lead to account harm.
Context confusionA value is encoded for one context but placed in another, such as HTML data inside a JavaScript string, URL, CSS block, or event-handler attribute.

Where XSS appears

XSS tends to appear anywhere user-controlled or externally supplied data is displayed. Treat stored content, API data, browser values, and third-party integration data as untrusted until the rendering path proves otherwise.

  • Search pages, filters, error messages, redirect notices, and confirmation screens that echo request values.
  • Comments, reviews, chat messages, profile fields, organization names, labels, tags, and custom metadata.
  • Support tickets, admin consoles, CRM notes, audit views, moderation queues, and internal dashboards.
  • Markdown previews, rich text editors, WYSIWYG content, HTML email previews, and document rendering.
  • URL fragments, query strings, local storage, session storage, cookies, postMessage events, and client-side routing state.
  • API-rendered content, GraphQL responses, mobile app data later reused on web pages, webhooks, imports, and partner feeds.
  • Analytics snippets, tag managers, browser extensions, third-party widgets, outdated UI libraries, and unsafe framework escape hatches.

How to prevent XSS

XSS prevention works best as layered engineering discipline. The primary fix is to keep untrusted data from becoming executable browser content. Browser headers, monitoring, and WAF rules can help, but they do not replace safe rendering.

Different browser contexts need different handling. HTML text, HTML attributes, JavaScript strings, CSS values, and URLs are parsed differently, so a one-size-fits-all escape function is not enough.

Use framework escaping by default

Prefer normal template interpolation and component bindings that render text safely. Avoid escape hatches unless the data is trusted or sanitized for that exact use.

Encode for the output context

Apply context-aware encoding for HTML text, attributes, URLs, CSS values, and JavaScript data. A value safe in one context may be unsafe in another.

Prefer safe DOM APIs

Use textContent, createTextNode, form value setters, and hardcoded safe attributes instead of innerHTML, document.write, inline handlers, or dynamic script construction.

Sanitize intentional rich HTML

When users must author HTML, use a maintained allow-list sanitizer, keep it patched, and avoid post-sanitization mutations that can undo the protection.

Validate URLs and identifiers

Allow-list URL schemes, route names, CSS options, and component identifiers instead of trusting arbitrary strings that can change browser behavior.

Use CSP as defense-in-depth

A strict Content Security Policy can reduce exploitability and provide reporting, but it should support safe rendering rather than compensate for unsafe rendering.

Harden sessions and storage

Use HttpOnly, Secure, and SameSite cookies where appropriate, minimize sensitive browser storage, and avoid exposing long-lived tokens to client-side code.

Detection and response

XSS testing must be performed only on systems you own or are explicitly authorized to assess. Effective detection combines source review, automated testing, browser inspection, and runtime signals.

Because XSS is context-dependent, scanners alone can miss issues or report findings that need manual validation. Review the exact source-to-sink path and confirm the intended safe rendering behavior.

  1. 1

    Map sources and sinks

    Inventory request parameters, stored fields, API data, browser storage, URL fragments, and third-party feeds, then trace where they are rendered in HTML, attributes, scripts, CSS, and DOM APIs.

  2. 2

    Review dangerous rendering paths

    Look for innerHTML, insertAdjacentHTML, document.write, unsafe framework APIs, raw markdown rendering, inline event handlers, dynamic script URLs, and custom template engines.

  3. 3

    Use authorized testing tools

    Run SAST, DAST, dependency checks, browser-based tests, and focused manual tests in environments where testing is permitted and observable.

  4. 4

    Watch runtime indicators

    Monitor CSP reports, unusual script errors, repeated suspicious input patterns, unexpected redirects, support reports, and sudden changes in form or account behavior.

  5. 5

    Patch and retest

    Replace dangerous sinks, add context-specific encoding or sanitization, rotate exposed tokens if needed, add regression tests, and retest the original path.

Developer checklist

Use this checklist before shipping any feature that displays user, integration, imported, or browser-sourced content.

  • Every untrusted value is rendered through a safe template binding or context-specific encoder.
  • No untrusted value reaches innerHTML, document.write, inline event handlers, script construction, or unsafe framework APIs.
  • Rich text is sanitized through a maintained allow-list sanitizer and is not modified unsafely after sanitization.
  • URLs are built with safe APIs, encoded correctly, and restricted to allowed schemes and destinations.
  • DOM-based flows are tested with URL fragments, query strings, storage values, postMessage, and API-provided data.
  • Cookies use HttpOnly, Secure, and SameSite settings where appropriate, and sensitive tokens are not stored unnecessarily in browser-accessible storage.
  • A strict Content Security Policy is used as defense-in-depth and monitored for suspicious reports.
  • Regression tests cover reflected, stored, and DOM rendering paths for the affected component.

XSS FAQ

What is an XSS vulnerability?

XSS is a browser-side injection flaw where untrusted data is rendered in a way that the browser can treat as active content inside a trusted application page.

What is the difference between reflected, stored, and DOM-based XSS?

Reflected XSS comes from the current request, stored XSS comes from saved content shown later, and DOM-based XSS is introduced by client-side JavaScript that writes unsafe data into the page.

Do modern frameworks prevent all XSS?

Modern frameworks reduce many common XSS bugs through escaping and safe bindings, but unsafe APIs, raw HTML rendering, untrusted URLs, outdated components, and custom DOM manipulation can still introduce XSS.

Is Content Security Policy enough to prevent XSS?

No. CSP is useful defense-in-depth and can reduce exploitability, but the durable fix is safe rendering through context-aware encoding, safe DOM APIs, and sanitization when rich HTML is required.

What is the best first step to prevent XSS?

Render untrusted content as text by default, use framework-safe bindings, avoid dangerous DOM sinks, and apply context-specific encoding or sanitization when data must appear in a special browser context.

References and further reading

These references informed this guide. The wording here is original and adapted for practical defensive education.