Join our Newsletter — 33% off our NHI Course
Home› Guides› HTML Sanitisation Guide: DOMPurify, Sanitizer API and Server-Side…
Guide Web & API Security

HTML Sanitisation Guide: DOMPurify, Sanitizer API and Server-Side Sanitisers

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 27 October 2026 15 min read
On this page

Any feature that lets users author rich text, paste formatted content or render HTML from a third party has to sanitise that markup, because output encoding would break the feature and leaving it raw invites cross-site scripting. Sanitisation is a different control from encoding: instead of turning every angle bracket into an entity, a sanitiser parses the markup, keeps an allowlist of elements, attributes and URL schemes, and drops the rest. This guide covers when to sanitise rather than encode, how DOMPurify and the browser's built-in HTML Sanitizer API work, which server-side libraries are maintained, and how to deploy, test and monitor a sanitiser in production. It is written for web developers, application security engineers and reviewers who own WYSIWYG editors, comment systems, email rendering, CMS features or Markdown pipelines.

Key takeaways

  • Sanitise only where users must author HTML. For every other value, contextual output encoding is the correct control; sanitisation is the exception for rich text, not a general input filter.
  • Use a maintained, parser-based library or the browser API. OWASP recommends DOMPurify on the client, and ASVS 5.0 requirement V1.3.1 asks for a well-known, secure library or framework feature for WYSIWYG input. Regex filters and home-made tag strippers fail.
  • Sanitise last, then do not touch the output. Modifying sanitised markup, or passing it to a library that re-parses or mutates it, can void the sanitisation. Sanitise as close to the sink as possible.
  • Prefer an allow configuration. List the elements, attributes and URL schemes the feature needs; do not try to enumerate what is dangerous.
  • Treat the sanitiser as a dependency with a patch SLA. Bypasses are found regularly, Bleach for Python stopped receiving security fixes in June 2026, and server-side DOM implementations such as old jsdom versions can reintroduce XSS on their own.

Sanitisation versus encoding: choosing the right control

The OWASP Cross Site Scripting Prevention Cheat Sheet is explicit about the split. Output encoding converts untrusted data into a safe form for the context where it is displayed (HTML body, attribute, JavaScript, CSS, URL). When users need to change the styling or structure of content inside a WYSIWYG editor, encoding would prevent XSS but also stop the styling from rendering, so HTML sanitisation should be used instead. A sanitiser strips dangerous HTML and returns a safe string of HTML.

SituationCorrect controlWhy
Name, comment or search term shown as textContextual output encoding (framework auto-escaping)No markup is needed, so none should survive
Rich text from a WYSIWYG editor (bold, lists, links)HTML sanitiser with an allowlistMarkup must render; only safe parts may
Markdown rendered to HTMLRender, then sanitise the HTML outputMost Markdown renderers pass raw HTML through; ASVS V1.3.5 covers Markdown, BBCode and stylesheets
User-uploaded SVGSanitise with an SVG profile, or rasterise, or serve from a sandboxed originSVG can carry script and foreignObject; ASVS V1.3.4
HTML email bodies or third-party widget HTMLSanitise, then render in a sandboxed iframe or separate originVolume and variety of markup make defence in depth worthwhile
URL supplied by a user and placed in hrefURL validation with a scheme allowlist, then attribute encodingASVS V1.2.2: allow only safe protocols, disallow javascript: and data:
Text inserted by client-side codetextContent or framework binding, not a sanitiserSafe sinks need no sanitisation at all

Framework escape hatches are where sanitisation usually becomes necessary. The OWASP cheat sheet lists React dangerouslySetInnerHTML without sanitising, Angular bypassSecurityTrustAs* functions and Lit unsafeHTML as common gaps. Angular has its own built-in sanitiser for [innerHTML] bindings; see the Angular Security Guide for that framework's specifics rather than adding a second sanitiser on top of it.

How an HTML sanitiser works, and why regex fails

A sound sanitiser follows the same sequence whatever the language:

  1. Parse: build a DOM tree using an HTML5-compliant parser, ideally the same parsing rules the browser will apply.
  2. Walk: visit every node, including SVG and MathML namespaces, templates and open shadow roots.
  3. Filter elements: keep allowlisted elements, drop or unwrap the rest (some configurations keep the text content of a removed element).
  4. Filter attributes: keep allowlisted attributes, always remove event handler attributes, and check URL-bearing attributes (href, src, action, xlink:href) against a scheme allowlist.
  5. Serialise or insert: return a string, a DOM fragment, or insert the nodes directly.

Regular expression filters fail because browsers do not read HTML the way a regex does. The DOMPurify project documents the attack classes its tests cover: mutation XSS (markup that changes meaning when parsed, serialised and parsed again), namespace confusion between HTML, SVG and MathML, DOM clobbering (elements whose id or name overwrite script variables), and raw-text breakouts. Each of these defeats a filter that only looks at the string.

This is also why the place you sanitise matters. Sanitising a string on the server, storing it, and then letting another component reformat it before rendering creates a gap between what the sanitiser saw and what the browser parses. The safest pattern is to sanitise at render time, at the sink, with the parser that will interpret the result.

DOMPurify in practice

DOMPurify is the library the OWASP XSS Prevention Cheat Sheet recommends for HTML sanitisation. The project README states it supports HTML5, SVG and MathML, allows CSS and custom data attributes by default, and sanitises templates and open shadow roots recursively. Version 2.5.9 is the last release supporting Internet Explorer; current development is on the 3.x line.

Configuration that matters

OptionEffectGuidance
USE_PROFILES: { html: true }Allows HTML only, no SVG or MathMLGood default for comment and rich text fields; overrides ALLOWED_TAGS
ALLOWED_TAGS, ALLOWED_ATTRReplace the default allowlistSet these to exactly what the editor toolbar produces
FORBID_TAGS, FORBID_ATTRRemove items from the default allowlistCommon: forbid style elements and attributes where CSS injection matters
ADD_TAGS, ADD_ATTR, ALLOW_UNKNOWN_PROTOCOLSWiden the allowlistReview every use; the README marks these as XSS risk
ALLOWED_URI_REGEXPControls permitted URL schemesDefault allows http, https, ftp, ftps, tel, mailto, callto, sms, cid, xmpp and matrix plus relative URLs; keep any custom regex linear-time
RETURN_TRUSTED_TYPE, TRUSTED_TYPES_POLICYReturn TrustedHTML or control the internal policyUse with a Trusted Types CSP; pass TRUSTED_TYPES_POLICY: null when calling from inside your own policy
SAFE_FOR_TEMPLATESStrips template expressions such as {{ }}The README says this mode is not recommended for production; avoid client-side template parsing of user HTML instead
SAFE_FOR_XMLHandling of comments with risky charactersLeave at the default true; setting it false is safe only for pure HTML with no SVG or MathML

Hooks and the removed property

  • Hooks: DOMPurify.addHook attaches functions to points such as uponSanitizeElement and afterSanitizeAttributes. A typical safe use is forcing rel="noopener noreferrer" and target="_blank" on links. Hooks run inside the sanitiser, so a buggy hook that re-adds attributes can reopen the hole: keep them small and tested.
  • Removed items: after sanitising, DOMPurify.removed lists what was thrown out. The README says not to use it for security decisions; it is useful as a telemetry signal (see Detection below).
  • Unsupported environments: in a browser DOMPurify cannot protect, it returns the input unchanged. Check DOMPurify.isSupported and fail closed (render as text) if it is false.

Running DOMPurify on the server

DOMPurify needs a DOM. On Node.js the project recommends jsdom and strongly recommends the latest version, because older jsdom releases have parsing bugs that produce XSS even when DOMPurify behaves correctly; the README cites known vectors in jsdom 19.0.0 fixed in 20.0.0. Pin and patch jsdom with the same urgency as DOMPurify itself. Server-side sanitisation with a non-browser DOM is a reasonable storage-time control, but keep client-side or render-time sanitisation as well where the markup is later inserted with innerHTML.

The HTML Sanitizer API in browsers

The browser-native HTML Sanitizer API removes the need to ship a library for many client-side cases. MDN describes two families of methods:

  • XSS-safe methods: Element.setHTML(), ShadowRoot.setHTML() and Document.parseHTML(). These always remove XSS-unsafe elements and attributes, even if a custom configuration allows them.
  • XSS-unsafe methods: Element.setHTMLUnsafe(), ShadowRoot.setHTMLUnsafe() and Document.parseHTMLUnsafe(). These apply only the configuration you pass; with no sanitiser they insert everything the context allows. They work with Trusted Types so they can still be audited.

MDN states that in most cases setHTML() with the default sanitiser can replace innerHTML directly. The XSS-safe baseline always removes embed, frame, iframe, object, script and SVG use elements, plus all event handler attributes. The default configuration is stricter still: it also removes items useful for clickjacking or spoofing, and removes comments and data-* attributes.

Configurations are either allow lists (elements, attributes) or remove lists (removeElements, removeAttributes), and you cannot mix allow and remove arrays for the same item type. MDN notes that an allow configuration is generally safer, because you list what you know is safe instead of what might be dangerous later.

Status and browser support (checked 27 October 2026)

  • Specification: the WICG draft says its content has been merged into the WHATWG HTML Standard and the draft should no longer be consulted for implementations.
  • Chrome and Edge: MDN browser compatibility data lists Element.setHTML() from Chrome 146, with notes that before version 153 the base element was not removed from a configuration's allowlist, and before 152 sanitisation ran after parsing rather than during it. Chrome 105 to 118 shipped an earlier, significantly different version of the API.
  • Firefox: supported from Firefox 148, with a compatibility note that base is not yet removed from a configuration's allowlist.
  • Safari: not supported at the time of checking.

The practical consequence: feature-detect ("setHTML" in Element.prototype), use setHTML() where available, and fall back to DOMPurify elsewhere. Do not pass a custom configuration that allows base while older engines are still in your support matrix. Browser support changes quickly, so re-check the compatibility data before relying on the native path alone.

Server-side sanitisers by language

Sanitising on the server is still needed where HTML leaves your control before a browser renders it: email, PDF generation, API consumers, mobile apps and feeds. Use a library built on a real HTML parser and an allowlist policy.

LanguageLibraryModelNotes
JavaOWASP Java HTML SanitizerPolicy built with HtmlPolicyBuilder or prepackaged Sanitizers.FORMATTING, Sanitizers.LINKSExplicit allowElements, allowUrlProtocols("https"), requireRelNofollowOnLinks(); attribute policies do not weaken default URL or style checks
Pythonnh3 (bindings to the Rust ammonia library)AllowlistThe nh3 README calls Bleach deprecated; Bleach's own README states it is no longer maintained as of 5 June 2026 with no future releases including security fixes
Node.jsDOMPurify with an up-to-date jsdomAllowlist, browser-like DOMPatch jsdom and DOMPurify together
RustammoniaAllowlistThe engine behind nh3
BrowserHTML Sanitizer API, DOMPurify fallbackAllowlist plus fixed XSS-safe baselineSee support notes above

If you use Bleach today, plan a migration: an unmaintained sanitiser is a vulnerability waiting for its first unpatched bypass. Record the library and version in your software bill of materials so the next advisory is matched quickly.

Implementation and rollout

  1. Inventory the sinks. Search the code for innerHTML, outerHTML, insertAdjacentHTML, document.write, dangerouslySetInnerHTML, bypassSecurityTrust, v-html, unsafeHTML and server templates marked safe (|safe, html_safe, raw). Each is a candidate for encoding, a safe sink, or sanitisation.
  2. Define one policy per feature. Derive the allowlist from what the editor toolbar can produce. A comment box rarely needs more than p, br, b, i, em, strong, ul, ol, li, a[href] and code. Write it down; review changes to it like code.
  3. Centralise the call. Wrap the sanitiser in one internal function or Trusted Types policy so every sink goes through the same configuration. See the Trusted Types and DOM XSS Prevention Guide; the OWASP cheat sheet suggests a default policy that delegates to a sanitiser for legacy code paths.
  4. Test against a corpus. Unit-test the wrapper with known-good content (every toolbar feature must survive) and with the project test vectors maintained by DOMPurify and the OWASP cheat sheets. Assert on the output DOM, not on string equality.
  5. Run in report mode first. For existing stored content, run the new policy over a copy of the data and log what it would remove. Review the diff with product owners before enforcing, because legitimate formatting may disappear.
  6. Enforce and add depth. Switch to enforcement, then add a Content Security Policy that blocks inline script and, on supporting browsers, require-trusted-types-for 'script'. See the Content Security Policy Guide.
  7. Keep a rollback. If enforcement breaks content, roll back to the previous policy version, not to no sanitisation. Store the raw input so content can be re-rendered under a corrected policy.

Decision guidance

  • Small teams: one library, one strict policy, framework auto-escaping everywhere else, and automatic dependency updates. Avoid custom hooks.
  • Large platforms with many rich text surfaces: a shared sanitisation package owned by the security engineering team, Trusted Types enforcement, per-feature policies, and a regression corpus run in CI.
  • Store raw or sanitised? Storing raw input and sanitising on output lets you fix policy mistakes retroactively; storing sanitised output protects every consumer that forgets. Many teams do both: sanitise on write for downstream consumers and again at render.
  • High-risk HTML (email, ads, third-party embeds): combine sanitisation with isolation, such as a sandboxed iframe or a separate content origin, because no allowlist is perfect.

Detection and measurement

Sanitisation is silent when it works, so instrument it:

  • Removal telemetry: log a count (not the content) of elements and attributes removed per request, from DOMPurify.removed or from a comparison of input and output length. Spikes of removed script, iframe or event handler attributes indicate probing.
  • CSP and Trusted Types reports: collect violation reports. A trusted-types violation names a sink that bypassed the wrapper.
  • Static analysis: CI rules that fail on new uses of raw HTML sinks or framework bypass functions outside the wrapper module.
  • Dependency alerts: track advisories for the sanitiser, jsdom and the Markdown renderer.
MetricTarget
Raw HTML sinks not routed through the approved wrapperZero, enforced by CI
Time from sanitiser security release to production deploymentWithin your critical patch SLA (for example 7 days)
Trusted Types or CSP script violations per weekTrending to zero, each one triaged
Features with a documented allowlist policy100% of rich text surfaces
Stored XSS findings in penetration tests and bug bountyDownward trend, root cause recorded

Common failure patterns

  • Sanitise, then mutate: running a sanitiser and then string-replacing, templating or passing the result through a library that re-parses it. DOMPurify's README names this as the main foot-gun. Sanitise last.
  • Sanitising the input to a Markdown renderer instead of its output: the renderer then produces HTML the sanitiser never saw. Render first, sanitise the HTML.
  • Allowing everything SVG: enabling SVG profiles for a comment field to support one icon, which brings in scriptable content. Serve images as images instead; ASVS V1.3.4 requires SVG to contain only safe tags and attributes.
  • Widening the allowlist under deadline pressure: adding style, iframe or ALLOW_UNKNOWN_PROTOCOLS to make one customer's content render. Every widening should pass security review.
  • Stale libraries and DOMs: an old DOMPurify or jsdom, or an unmaintained library such as Bleach, left in place because "it already sanitises". The OWASP cheat sheet stresses regular patching because bypasses are discovered regularly.
  • Using a WAF as the sanitiser: OWASP states WAFs are unreliable for XSS and miss client-side DOM XSS entirely.

Framework mapping

FrameworkReferenceRelevance
OWASP ASVS 5.0V1.3.1, V1.3.4, V1.3.5, V1.2.2, V1.1.2Sanitise WYSIWYG HTML with a secure library; SVG and Markdown sanitisation; safe URL schemes; encode or sanitise as the final step
NIST SP 800-53 Rev. 5SI-10 Information Input Validation, SA-11 Developer Testing and Evaluation, SI-2 Flaw RemediationValidation of untrusted input, testing the policy, patching the library
NIST CSF 2.0PR.PS-06Secure software development practices
CIS Controls v8.1Control 16 Application Software SecuritySecure coding, third-party component management
ISO/IEC 27001:2022 Annex A8.28 Secure codingSecure coding principles applied to development
MITRE CAPECCAPEC-63 Cross-Site Scripting (XSS)The attack pattern sanitisation defends against

For the wider XSS picture, including encoding rules per context, see the Cross-Site Scripting (XSS) Prevention Guide.

Practitioner checklist

  • List every raw HTML sink and framework bypass in the codebase and classify each as encode, safe sink or sanitise.
  • Write an allowlist policy per rich text feature, derived from what the editor produces, and keep it in version control.
  • Route all sanitisation through one wrapper or Trusted Types policy; block new raw sinks in CI.
  • Use setHTML() where supported and DOMPurify as the fallback; fail closed when DOMPurify.isSupported is false.
  • Render Markdown first, then sanitise the HTML output.
  • Replace unmaintained sanitisers such as Bleach; pin and patch DOMPurify and jsdom together.
  • Sanitise at the sink and never modify the sanitised output afterwards.
  • Add CSP and Trusted Types, and collect their violation reports.
  • Log counts of removed elements and alert on spikes of dangerous tags or event handlers.
  • Isolate the highest-risk HTML (email, third-party embeds) in a sandboxed frame or separate origin.

Standards and references

Related NHI Mgmt Group resources: Cross-Site Scripting (XSS) Prevention Guide · Trusted Types and DOM XSS Prevention Guide · Content Security Policy Guide · Angular Security Guide · File Upload Security 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
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 27 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org