Subscribe
Skip to content
Subscribe

Application Security

Best XSS defenses for the software we ship (2026)

A 2026 shortlist of sanitizers, encoders, and browser controls that still close XSS sinks. Public docs, not a lab bake-off.

Expertise: Application Security · Level: Intermediate · 16 min read

A sanitizer is not a policy.

The sink is still the same. A comment preview, a CMS body, a Vue slot: someone assigned a string to innerHTML, v-html, or an unescaped template, and the browser parsed it as the page. Cookie flags do not close that hole. The XSS guide walks the jobs and the tests. This page is the shortlist of controls that still map to those sinks.

Six defenses. Sanitizers for HTML that must render, browser locks on the DOM sinks, a CSP that can stop injected script, and encoders for text on Node and the JVM.

Technical check: we cross-check project READMEs, W3C/MDN, and the live XSS guide. Rank is a technical recommendation, not a recap of other lists, and not a lab bake-off.

DefenseBest forLicense
DOMPurifyHTML that must renderApache-2.0
Trusted TypesLocking innerHTML, eval, and script URL sinksPlatform policy
CSP nonce + strict-dynamicStopping injected script from runningHTTP response policy
heEncoding HTML text and attributesMIT
sanitize-htmlServer-side HTML allowlists in NodeMIT
OWASP Java EncoderContextual encoding on the JVMBSD
Where it sits
Text encoders
Encode on the platform None on this list
HTML sanitizers
Browser locks
1

DOMPurify

Best for HTML that must render

DOMPurify

DOMPurify is the sanitizer most teams already ship when a rich-text field has to become HTML. The README is explicit: it is a sanitizer, not an encoder, and the hook API is how you keep the policy tight. Pair it with Trusted Types if the sink is still innerHTML.

Key features

  • HTML, SVG, and MathML sanitizer for the browser and Node
  • Hook API to forbid tags or rewrite URLs before they land
  • Documented Trusted Types support so a policy can mint HTML only from DOMPurify
  • Default profile strips script and event handlers; you still name the tags you want

Why we like it

Use it when the product decision is already “we will render HTML.” Do not use it as a substitute for encoding text. If the value is text, encode. If it must be HTML, sanitize with a named policy.

leipert wrote it in July 2021: “I would highly recommend using DOMPurify over sanitize-html. It is a lot smaller in bundle size, it is also well maintained.” And as soon as links are involved, “please stand on the shoulder of giants in order to prevent XSS.” That is the argument for a maintained sanitizer, not a homemade strip of tags.

Limits

It cannot save a page that builds script URLs or CSS expressions from the same string. Server-rendered markup still needs a server sanitizer or encoder. The default profile is a start, not a threat model.

License

Apache-2.0. Project home is the Cure53 repository.

2

Trusted Types

Best for locking innerHTML, eval, and script URL sinks

Trusted Types

Trusted Types is a browser policy, not a vendor. Once require-trusted-types-for 'script' is on, innerHTML and similar sinks reject raw strings. A small policy that only your sanitizer can mint is the shape that still holds.

Key features

  • CSP require-trusted-types-for and trusted-types directives
  • Blocks string assignment to innerHTML, outerHTML, script src, and eval-like sinks
  • Policies return TrustedHTML, TrustedScript, or TrustedScriptURL
  • You write the policy. The browser does not sanitize for you

Why we like it

This is the control that turns a sanitizer from advice into a runtime TypeError. Turn it on after you know which sinks you still need. The XSS article is the note. This is the lock.

Limits

Enforcement landed in Chromium first. A polyfill does not create a security boundary in a browser that ignores the policy. You still need encoding or a sanitizer to mint the trusted values.

License

Platform policy. Specified by the W3C Trusted Types draft.

3

CSP nonce + strict-dynamic

Best for stopping injected script. Not a substitute for encoding.

Content Security Policy

A per-response nonce plus strict-dynamic is still the CSP shape that holds after the framework defaults. script-src must arrive on the document response. A meta tag after a script has already run is not a defense.

Key features

  • script-src 'nonce-…' 'strict-dynamic' for scripts the page meant to run
  • object-src 'none' and base-uri 'none' as the usual companions
  • Report-Only first, then enforce, so you can see the breaks
  • Works with Trusted Types on the same Content-Security-Policy header

Why we like it

CSP is not an XSS eraser. It is the net under the encoder and the sanitizer. If you cannot nonce the first-party scripts, hashes are the other honest option. unsafe-inline is a comment, not a policy.

Limits

JSONP, widget iframes, and old tag managers fight strict-dynamic. A policy that adds unsafe-inline to silence the console has already lost. CSP does not encode output.

License

HTTP response policy. Specified in CSP Level 3.

4

he

Best for encoding HTML text and attributes. Not a sanitizer.

he

Encoding is the first defense when the sink is text. he covers the entities a browser will still parse, including the ones people forget when they roll a replace list. It does not sanitize markup. If you need tags, use a sanitizer.

Key features

  • encode and decode for HTML text and quoted attributes
  • useNamedReferences and allowUnsafeSymbols documented in the README
  • Works in browsers and Node. Small. No DOM required
  • Does not invent a tag allowlist

Why we like it

Reach for he, or the platform equivalent, whenever the value is not allowed to become structure. That is most of the XSS you will still see: a name, a title, a URL path printed into HTML.

Limits

Wrong context still loses. JavaScript string context, CSS, and script URL attributes are not HTML-text problems. he will not save innerHTML of a “safe subset” you hoped to keep.

License

MIT. Project home is the mathiasbynens/he repository.

5

sanitize-html

Best for server-side Node allowlists

sanitize-html

sanitize-html is the common Node allowlist when HTML is stored, mailed, or rendered on the server. The default tag list is a starting point. Public docs tell you to name every tag and attribute you actually need.

Key features

  • Allowlists for tags, attributes, and schemes
  • Transform and exclusiveFilter hooks for links and leftover markup
  • Meant for Node. Not a browser Trusted Types policy
  • Used as the server half when the client runs DOMPurify

Why we like it

Put this on the write path, not only on the way out. Stored XSS is still the usual miss: a field saved raw and printed later in a different template.

Limits

A browser copy is not the same library. It will not lock innerHTML by itself. If the allowlist includes style or event-handler attributes, you have rebuilt the bug. GitHub archived apostrophecms/sanitize-html on February 26, 2026 and marked the repo deprecated in favor of the ApostropheCMS monorepo.

License

MIT. Project home is the ApostropheCMS repository.

6

OWASP Java Encoder

Best for contextual encoding on the JVM

OWASP Java Encoder

The Java Encoder is still the checklist item for JSP and server-rendered sinks. Encode for HTML, attributes, JavaScript, and CSS separately. It is not a sanitizer for rich HTML, and it should not be asked to be one.

Key features

  • forHtml, forHtmlAttribute, forJavaScript, forCssString, and related helpers
  • Designed so you pick the context at the sink
  • No default “clean this HTML” API, which is the point
  • Pairs with the OWASP secure coding checklist we already publish

Why we like it

If the stack is Java and the page is server-rendered, this is the encoder. For rich text, pick a Java HTML sanitizer with an explicit policy and keep the encoder for everything else.

Limits

Wrong helper, wrong context. forHtml inside a script tag is not a defense. It will not help a React or Vue sink. Those stacks have their own defaults, which you can still break.

License

BSD (New BSD). Project home is the OWASP Java Encoder page.

What the internet thinks about XSS defenses

Live threads. We quoted the argument, not the score.

Hacker News

“Trusted Types will prevent a dependency or careless developer from setting innerHTML without going through a policy you’ve evaluated and decided to trust, but it doesn’t have an HTML sanitizer, so for those cases a library like DOMPurify is still necessary.”

rictic, 25 Sep 2019, from the thread on Heiderich’s DOMPurify talk. The lock and the sanitizer are two jobs.

Stack Overflow

“Likely yes, but it’s not 100% guaranteed. If DOMPurify doesn’t have bugs that will let XSS through, setting innerHTML or dangerouslySetInnerHTML with its results will be safe.”

CertainPerformance on sanitized innerHTML. A sanitizer is a library, not a proof.

Information Security

“CSP works by enforcing that certain content policies are placed upon scripts, e.g. “no external scripts”, or “no inline scripts”. This makes XSS a whole lot harder, because 99% of XSS cases involve inline scripts or references to off-site scripts.”

Polynomial on CSP as a net. The same thread notes a strict policy is hard to ship on a JavaScript-heavy site. That is why nonce plus strict-dynamic exists.

FAQs

How do CSP, Trusted Types, and a sanitizer share the work?

They are three different jobs. A sanitizer (DOMPurify, sanitize-html) decides which HTML may exist. Trusted Types refuse a raw string at innerHTML, eval, and script URL sinks. CSP nonce + strict-dynamic decides which scripts the document is allowed to run. Encode text first. Then pick the control that matches the sink that is still open.

When do I use he versus the OWASP Java Encoder?

Same job, different runtime. he encodes HTML text and attributes in JavaScript and Node. The OWASP Java Encoder does contextual encoding on the JVM: HTML, attributes, JavaScript, CSS. Neither is a sanitizer. If the sink is text, encode with the library that lives next to that sink.

If I encode every sink, can I skip CSP?

No. Encoding is the primary defense at the sink you own. CSP is the net for the script you did not mean to run: a missed hatch, a third-party widget, a future template. Skip CSP and one forgotten innerHTML becomes a running script. Keep both.

Is this a scored bake-off?

No. Order is editorial. We did not install these in a lab or assign points. A defense is here if it is widely shipped, has a public policy or API, and maps to a sink the XSS guide already names.