Join our Newsletter — 33% off our NHI Course
Home› Guides› XSS Prevention Guide: Stored, Reflected and DOM-Based XSS
Guide Application Security

XSS Prevention Guide: Stored, Reflected and DOM-Based XSS

← All guides
By Lalit Choda, NHI Mgmt Group Updated 18 October 2026 17 min read
On this page

Cross-site scripting (XSS) lets untrusted data run as script in another user's browser, inside your origin and with that user's session, and it remains one of the most frequently reported web weaknesses. OWASP counts more than 30,000 CVEs for XSS in the Top 10:2025 Injection category, and CISA and the FBI have issued a Secure by Design Alert asking software makers to eliminate the class rather than patch instances. This guide goes deeper than the overview in the Web Application Attacks Guide: it explains how stored, reflected, DOM-based, blind and mutation XSS differ, where each fix belongs, how to run HTML sanitisation and Trusted Types safely, and how to test, detect and measure progress. It is for application security engineers, front-end and back-end developers and the reviewers who sign off releases, and it is written from the defender's side, with no payloads or attack procedures.

Key takeaways

  • Fix XSS where the data is rendered. Contextual output encoding at the last step before the browser parses the data, done by the template engine or a vetted encoder, removes most server-side XSS; input filtering does not.
  • DOM-based XSS needs browser-side controls. The server may never see the data, so safe DOM APIs, a reviewed set of sinks and Trusted Types are the defences, and server-side scanners and WAFs largely miss it.
  • Sanitise rich HTML once, then leave it alone. Use a maintained HTML sanitiser with an allowlist configuration and never modify its output; mutation XSS exploits the gap between what a sanitiser parsed and what the browser later builds.
  • CSP limits damage, it does not fix bugs. A strict nonce or hash policy blocks most injected script, but allowlist policies are routinely bypassed, so CSP sits behind encoding and sanitisation, never in front.
  • Blind XSS targets your staff. Payloads stored in tickets, logs and admin consoles fire in privileged sessions; back-office tools need the same encoding, sanitisation and CSP as customer-facing pages.

How XSS works and the main variants

CWE-79, Improper Neutralization of Input During Web Page Generation, describes the root cause of every variant: data from an untrusted source ends up in a page without being neutralised for the context it lands in, so the browser treats it as markup or code. The script runs in your origin, so it can read what the page reads, call your APIs with the user's cookies and rewrite what the user sees. OWASP rates XSS high frequency and relatively low impact, but one stored XSS in an admin console can hand over a platform.

VariantWhere the malicious data livesWho it hitsWhere the fix belongsMain test reference
Reflected XSS (non-persistent, CWE-79 Type 1)In the request, echoed straight back in the responseA user who follows a crafted link or submits a crafted formServer-side output encoding in templatesWSTG-INPV-01
Stored XSS (persistent, Type 2)Saved in a database, file, log or message and rendered laterEvery user who views the content, often administratorsEncoding at render time; sanitisation for rich textWSTG-INPV-02
DOM-based XSS (client-side XSS, Type 0)Read by JavaScript from the URL, fragment, storage or messages and written to a sinkUsers of the single-page app or scriptSafe DOM APIs, Trusted Types, client-side sanitiserWSTG-CLNT-01
Blind XSSStored in a place the attacker never sees, such as a support queue or log viewerSupport agents, moderators, administratorsEncoding and CSP in internal and third-party back-office toolsStored XSS tests with out-of-band confirmation
Self-XSSTyped or pasted by the victim, usually after social engineeringThe person persuaded to paste itUser warnings in consoles; fix as a real bug if any other user can trigger itWSTG-CLNT-01.1
Mutation XSS (mXSS)Markup that is harmless as sent but changes when the browser parses and reserialises itUsers of pages that sanitise and then reinsert HTMLSanitise in the same parser context as insertion; keep sanitisers patchedSanitiser regression tests
Universal XSS (UXSS)A flaw in the browser or a browser extension, not in your applicationAny site the affected browser visitsBrowser and extension patching, extension allowlistsOut of application scope

Stored XSS is harder to find than reflected because, as CWE-79 notes, injection and activation can be separated by minutes or days. WSTG has a separate test for self DOM-based XSS, and a "self" bug can become exploitable when chained with login CSRF. Universal cross-site scripting belongs to browser management; see the Enterprise Browser and Browser Security Guide.

Injection points teams miss

Most XSS reviews concentrate on form fields. The CWE-79 child entries and CAPEC patterns show where the rest come from.

  • HTML attributes (CWE-83, CAPEC-243): unquoted values let data break out without angle brackets. The OWASP XSS Prevention Cheat Sheet says always quote attribute values, and attribute encoding alone does not make data safe inside event-handler attributes. Keep untrusted data out of event handlers and style attributes.
  • XSS through HTTP headers (CAPEC-86): CWE-79 lists request headers as untrusted input, with observed cases through a crafted header and the Referer header in statistics and log analysis tools. Dashboards and log viewers must encode headers like any other input.
  • XSS targeting error pages (CWE-81, CAPEC-198): error pages that repeat the requested path or parameter are a classic reflected source. Use auto-escaping error templates and keep request data out of error messages.
  • XSS using MIME type mismatch (CAPEC-209): a browser that decides a file's real type differs from the declared one may run it as script. Send an accurate Content-Type and charset with X-Content-Type-Options: nosniff, serve uploads from a separate domain as attachments, and serve JSON as application/json, never text/html.
  • XSS in Markdown: many renderers pass raw HTML through and accept any link scheme. ASVS 5.0 requirement 1.3.5 asks that user-supplied Markdown, CSS, XSL and BBCode be sanitised or disabled: turn off raw HTML, allow only http, https and mailto links, and sanitise the rendered output.
  • SVG uploads: ASVS 1.3.4 requires user-supplied SVG to contain only safe tags and attributes, excluding scripts and foreignObject; rasterising removes the problem.
  • "Internal" data: the cheat sheet says data from internal sources "should always be considered tainted"; apply that to partner feeds, CRM records and AI model output.

Context-specific output encoding in practice

The Secure Coding Guide sets out the encoding rule for each output context (HTML body, attribute, JavaScript, CSS, URL and DOM). This section covers where secure output encoding fails in real code bases even when developers know those rules.

  • Encode last, decode once: ASVS 5.0 requirement 1.1.2 asks for encoding as the final step before the interpreter, and 1.1.1 for input to be decoded to canonical form only once. Encoding on the way into the database produces double-encoded text in some views and raw text in exports and emails.
  • No global filters: the OWASP cheat sheet rejects interceptors that encode every request parameter: their encoding is "completely non-contextual", they double encode and they do nothing for DOM-based XSS.
  • Know your escape hatches: auto-escaping stops at helpers such as React dangerouslySetInnerHTML, Angular bypassSecurityTrust methods, Django mark_safe and Lit unsafeHTML. React 19 blocks javascript: URLs, but this "does not replace application-specific URL validation". Lint each helper and require a reviewed justification.
  • Allow only safe URL schemes: ASVS 1.2.2 requires blocking javascript: and data: URLs. Compare the parsed scheme against an allowlist; string prefix checks fail on case and whitespace variations.
  • Keep data out of code: an encoded string later passed to eval or a similar sink "remains unsafe". Pass server data as JSON in a data attribute and read it with JSON.parse.
  • Declare the character set: CWE-79 recommends an explicit character encoding on every page so the browser does not guess.

HTML sanitisation and mutation XSS

Encoding turns markup into text, which is wrong when users are meant to submit formatting, such as comments with links, knowledge base articles or email templates. Here the control is HTML sanitisation (HTML sanitization in US spelling): parse the markup, keep only allowlisted elements and attributes, and drop the rest. ASVS 1.3.1 requires untrusted HTML from WYSIWYG editors to be sanitised with "a well-known, secure sanitization library or framework feature".

Sanitiser optionStrengthsLimitsChoose when
Browser HTML Sanitizer API (Element.setHTML)Browser's own parser; safe methods "always remove XSS-unsafe elements and attributes"MDN lists it as limited availability, not BaselineProgressive enhancement now; default once supported browsers ship it
Maintained client-side sanitiser librarySame parser that renders the result, reducing mutation riskFrequent patching; output must not be modifiedSingle-page apps inserting HTML
Server-side sanitiser libraryClean at rest for every consumer (web, email, export)Server parser can differ from browsers, the root of mutation XSSContent rendered in many places
Restricted markup language (Markdown with raw HTML off)Small, predictable feature setRenderer settings and link schemes still need reviewComments, tickets, documentation

Mutation-based cross-site scripting was described by Heiderich and colleagues at ACM CCS 2013: markup that is harmless as sent changes when a browser parses it, serialises it through innerHTML and parses it again, so it passed server-side filters, client-side filters and WAF and IDS systems. The defences follow from the mechanism:

  • Do not post-process sanitised HTML: OWASP warns that modifying sanitised content "can easily void your security efforts", including changes made by downstream libraries.
  • Sanitise at insertion, in the target context: sanitising in the browser where the HTML is inserted, or with the context-aware Sanitizer API, removes the reparse step mXSS depends on.
  • Prefer allowlist configurations: MDN says an allow configuration "is safer" than a remove configuration.
  • Patch sanitisers as security dependencies: mutation bypasses are fixed in sanitiser releases, so track the library in software composition analysis.

DOM-based XSS and Trusted Types

The OWASP DOM based XSS Prevention Cheat Sheet puts it plainly: "DOM-based XSS arises from unsafe client-side processing." Data flows from a source (the URL and fragment, postMessage events, web storage, API responses) to an injection sink. Fragment data never reaches the server, so server-side encoding, WAFs and most scanners cannot see it. MDN groups sinks as HTML sinks (innerHTML, document.write), JavaScript sinks (eval) and JavaScript URL sinks (a script element's src).

DOM-based XSS prevention starts with safe APIs: textContent instead of innerHTML, setAttribute only for attributes that cannot execute code, JSON.parse instead of eval, and functions rather than strings passed to setTimeout, setInterval and event handler properties.

Trusted Types turns those rules into browser enforcement. With the CSP directive require-trusted-types-for 'script', a plain string passed to an injection sink throws a TypeError; only objects produced by a named policy are accepted. MDN marks the API "Baseline 2026 Newly available", across current browsers since February 2026, and OWASP says it "eliminates entire classes of DOM XSS rather than mitigating them".

  1. Inventory sinks: find sink use in first-party code and list third-party scripts that write HTML.
  2. Report first: send require-trusted-types-for 'script' in a Content-Security-Policy-Report-Only header for at least one release cycle.
  3. Refactor: replace string sinks with safe APIs; where HTML is genuinely needed, create one small policy whose createHTML calls your sanitiser.
  4. Default policy only for transition: MDN recommends it only while moving off legacy code, because it accepts strings at every sink.
  5. Enforce and pin names: enforce the directive and list allowed names in trusted-types so injected code cannot register its own policy. Roll back a breaking route to report-only, not the whole CSP.

CSP as a second layer, and how it gets bypassed

Content Security Policy is owned by the Security Headers Guide, which covers directives, nonces, hashes and rollout. For XSS the point is its place in the stack: OWASP says CSP "should not be your primary defense mechanism" and web.dev calls it a defence-in-depth technique that does not replace fixing XSS bugs. A content security policy bypass usually comes from the policy, not from a browser flaw:

  • Host allowlists: web.dev says allowlist policies "can be bypassed by attackers", because an allowed CDN or analytics host often serves old libraries, JSONP endpoints or user content. Nonces or hashes with 'strict-dynamic' remove host trust.
  • 'unsafe-inline' or 'unsafe-eval': either gives back most of what CSP added.
  • Missing object-src 'none' and base-uri 'none': injected plugin content and base tags redirect script loading.
  • Nonce misuse: nonces reused, cached with the page, or added by middleware to every script tag, including injected ones.
  • Script gadgets: trusted framework code that turns attacker-controlled markup into script is still allowed by the policy; Trusted Types closes much of this gap.

Testing, scanning and detection

CWE-79 rates both automated static analysis and black-box testing as "Moderate" in effectiveness, so XSS scanning needs more than one technique. Match the method to the variant.

MethodFinds wellMissesUse it for
SAST with taint trackingRequest data reaching templates without encoding; raw-output helpers; dangerous DOM sinksFramework auto-escaping it does not model; data stored by another serviceEvery pull request, blocking new high findings
DAST (reflection-based scanning)Reflected XSS, some stored XSS on crawled pagesDOM-based XSS: WSTG says automated testing has "only very limited success"; stored XSS that fires in back-office pagesStaging builds and regression
Browser-instrumented or DOM-aware testingSource-to-sink flows in client code, Trusted Types violationsPaths not exercised by the crawlSingle-page apps
Blind XSS testing with out-of-band callbacksStored input that executes later in support, moderation and admin toolsNothing fires if internal tools are out of scopeAuthorised tests that include back-office applications in scope
Manual reviewEscape hatches, URL handling, sanitiser configuration and post-processingVolumeRich-text features, templating changes, new sinks

What to monitor

  • CSP and Trusted Types violation reports: a burst of script-src or trusted-types violations from one page or tenant is an injection attempt or a compromised third-party script. Send reports to the SIEM with page, directive, blocked URI and sample, and rate-limit the endpoint. See the SIEM Guide.
  • Stored content signals: markup appearing in fields that never legitimately hold it, especially tickets, profile fields and file names viewed by staff.
  • Session anomalies: admin API calls or token creation straight after a support agent opens a ticket.
  • WAF events as a signal only: OWASP calls WAFs "unreliable" for XSS; their logs show probing, not coverage.

Metrics

  • Open XSS findings by variant and median days to fix.
  • Share of applications with zero unreviewed escape hatches.
  • Share of applications, including back-office tools, enforcing a strict CSP and Trusted Types.
  • CSP violations per 10,000 page views, falling during refactoring and flat after enforcement.
  • Days between a sanitiser security release and its deployment.

Where identity fits

XSS is a session and token problem as much as a content problem. MITRE ATT&CK T1539 Steal Web Session Cookie describes adversaries stealing cookies by "injecting malicious JavaScript content into websites". HttpOnly cookies stop script reading the cookie, but injected script can still act inside the session, call APIs and read any bearer token kept in web storage. The Canvas Instructure breach is a textbook blind XSS: according to Instructure, code submitted in a support ticket ran when an agent opened it and yielded an authorisation token with elevated access, and a second XSS in the discussion feature was used later to mint a new token.

  • Keep tokens out of reach of script: prefer HttpOnly, Secure, SameSite cookies or a backend-for-frontend over access tokens in localStorage. See the Token and Session Security Guide.
  • Step up privileged actions: require re-authentication for token creation, role changes and exports, so script in a staff session cannot complete them silently.
  • Isolate back-office tools: show customer content to agents in a sandboxed origin or as plain text.

Rollout and decision guidance

  1. Baseline: list applications, rich-text features and back-office tools that render customer data; tag findings by variant and CWE-79 child.
  2. Close server-side classes: enforce auto-escaping, lint escape hatches, remove global filters, fix error templates.
  3. Standardise sanitisation: one approved sanitiser per platform behind a shared helper, plus Markdown and SVG rules.
  4. Tackle DOM sinks and CSP: Trusted Types and a strict CSP, each report-only first, enforced in rings, with per-route rollback.
  5. Gate releases: SAST on merge, DAST and DOM-aware tests before release, ASVS V1 as acceptance criteria.
  • Encode or sanitise: encode whenever data is meant to be text; sanitise only fields that must carry markup, and keep that list short.
  • Server or client sanitiser: client-side at insertion for browser rendering; server-side where content feeds many consumers, then again in the browser.
  • Trusted Types or CSP first: single-page apps with many sinks gain most from Trusted Types; server-rendered apps from a strict CSP.
  • Small versus large organisations: small teams rely on framework defaults, an escape-hatch lint rule and an edge-set CSP; large ones publish approved encoder and sanitiser wrappers and track findings by variant across the portfolio.

Framework mapping

FrameworkReferenceWhat it asks for
OWASP ASVS 5.0V1.1.1, V1.1.2, V1.2.1, V1.2.2, V1.2.3, V1.3.1, V1.3.4, V1.3.5Canonicalise once, encode last, context encoding, safe URL schemes, sanitiser library, SVG and Markdown handling
OWASP Top 10:2025A05:2025 InjectionXSS (CWE-79) reported under Injection
MITRE CWECWE-79; children CWE-80, 81, 83, 84, 85, 86, 87Root cause and variant tagging for findings
NIST SP 800-53 Rev. 5SI-10, SI-15, SA-11Input validation, information output filtering, developer testing
CIS Controls v8.116.1, 16.12Secure application development process, code-level security checks
ISO/IEC 27001:2022 Annex AA.8.28, A.8.29Secure coding, security testing in development and acceptance
MITRE ATT&CK v20T1539 Steal Web Session CookieSession theft that XSS enables

Common failure patterns

  • Back-office tools left out of scope: support consoles render customer input with less care than public pages, which is what the Canvas support-ticket XSS exploited.
  • Sanitise, then transform: sanitised HTML passed through a Markdown step or link rewriter reopens the hole.
  • Closing findings because the WAF blocks the test string: the flaw stays reachable through another encoding, a JSON API or a DOM source.
  • An allowlist CSP counted as protection: a policy trusting a public CDN or keeping 'unsafe-inline' passes audits and stops little.
  • Stale sanitiser versions: the library is pinned at the release before last year's mutation fix.

Practitioner checklist

  • Use auto-escaping templates and lint every raw-output helper.
  • Remove global encoding filters; encode at render time for the exact context.
  • Serve accurate Content-Type and charset, nosniff, JSON as application/json, and uploads from a separate domain.
  • Allow only http, https and mailto schemes in user-supplied links, checked on the parsed URL.
  • Sanitise rich HTML, Markdown output and SVG with one approved, patched sanitiser, and never modify its output.
  • Replace string DOM sinks with safe APIs and enforce Trusted Types after report-only.
  • Enforce a strict CSP and send CSP and Trusted Types reports to the SIEM.
  • Include support and admin tools in XSS testing, with blind XSS checks.
  • Keep session tokens out of web storage and step up privileged actions.
  • Track XSS findings by variant, time to fix and recurrence.

Standards and references

Related NHI Mgmt Group resources: Web Application Attacks Guide · Secure Coding Guide · Security Headers Guide · Token and Session Security Guide · Penetration Testing Guide

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Written and reviewed by Lalit Choda, NHI Mgmt Group. Last updated 18 October 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org