mrkeyoor.com_
Thu 06 Aug 00:58 UTC
npmSecurityupdated 05 Aug 2026

dompurify

DOMPurify takes a string of untrusted HTML and gives you back a string with everything dangerous removed: script tags, event handler attributes, javascript: URLs, and the long tail of parser tricks that turn markup into code execution. It does this by parsing the input with the browser's own HTML parser and walking the resulting tree against an allow-list, which is why it handles malformed input the same way the browser will. It covers HTML, SVG, and MathML by default, has no dependencies, and is written and maintained by the security researchers at Cure53 who also publish the attack research behind it. On the server it needs a DOM implementation such as jsdom.

Verdict

If you must render untrusted HTML in a browser, this is the library, and it is the rare dependency where the maintainers are the same people publishing the attack research. Treat upgrades as security patches, never post-process the output, and keep jsdom current if you run it server-side.

API stability5/5sanitize(dirty, config) plus addHook and setConfig has been the shape since 1.x. The 3.x line dropped legacy browser workarounds and moved to modern module exports, but existing calls kept working, and only SAFE_FOR_JQUERY has ever been removed from the config list.
Docs4/5One very long README documents every config flag with an inline example, backed by wiki pages on the threat model, the default allow-list, and past bypass classes. There is no searchable API reference, so finding a specific flag means scrolling, and the demos folder carries knowledge the README does not.
Maintenance5/53.4.13 shipped two days before this review, releases are frequent, the tracker sits at zero open issues, and there is a security contact plus a bug bounty through Fastmail for working bypasses.
Ecosystem5/5About 60M weekly downloads and 17k stars, bundled or recommended by most rich-text editors and framework docs, with wrappers such as isomorphic-dompurify and vue-dompurify-html around it.

Use it if

  • You render HTML you did not author (comments, rich text editor output, email bodies, third-party feeds) into innerHTML or dangerouslySetInnerHTML
  • You need the sanitizer to agree with the browser's parser, including mutation XSS cases where naive regex or string filters see different markup than the browser does
  • You want a configurable allow-list: ALLOWED_TAGS, ALLOWED_ATTR, USE_PROFILES, and hooks to add rules like forcing rel=noopener on links
  • You are adopting Trusted Types and need a sanitizer that can act as the createHTML implementation for your policy
Skip it if

Setup reality

npm install dompurify and call DOMPurify.sanitize(dirty) in a browser and you are done: no dependencies, no build step, ESM and CJS both exported, types included. Server-side is where the work is. You install jsdom too, build a window, and pass it to the factory, and you must keep jsdom current because a stale DOM implementation can hand DOMPurify markup the real browser would parse differently. Bundlers occasionally trip on the dual purify.es.mjs and purify.cjs.js exports in mixed ESM and CJS test setups, and the isomorphic-dompurify wrapper exists mainly to paper over that. If your page sets a strict Content Security Policy with trusted-types, DOMPurify's attempt to create its own internal policy named dompurify gets blocked and logs a violation until you either allow that name or pass TRUSTED_TYPES_POLICY: null. Finally, defaults allow SVG and MathML; most apps want USE_PROFILES: { html: true } instead.

Patterns

Sanitize a string of HTMLbasic-sanitize

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirty);
container.innerHTML = clean;

The default config allows HTML, SVG, and MathML. Assign the result immediately; anything that edits the string afterwards can undo the sanitization.

Allow plain HTML but no SVG or MathMLhtml-only-profile

const clean = DOMPurify.sanitize(dirty, {
  USE_PROFILES: { html: true },
});

Most apps never need SVG or MathML, and both have carried bypass classes of their own. USE_PROFILES overrides ALLOWED_TAGS, so do not pass the two together.

Restrict to a small set of tags and attributesstrict-allowlist

const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'code'],
  ALLOWED_ATTR: ['href', 'title'],
});

An explicit allow-list is the strongest configuration available here. Note that KEEP_CONTENT defaults to true, so text inside a removed tag survives even though the tag does not.

Render sanitized HTML in Reactreact-inner-html

import DOMPurify from 'dompurify';

function Comment({ html }) {
  const clean = DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Sanitize at render time, not once at fetch time into state that other code may mutate. During server rendering this same component needs a DOM, so guard it or use the isomorphic wrapper.

Run it in Node with jsdomserver-side-jsdom

import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';

const window = new JSDOM('').window;
const purify = DOMPurify(window);

const clean = purify.sanitize('<img src=x onerror=alert(1)>');

Create the window once and reuse the instance; building a JSDOM per request is slow. Keep jsdom on its latest version, and do not substitute happy-dom, which the maintainers say is not safe for this.

Force rel and target on every linksafe-links-hook

DOMPurify.addHook('afterSanitizeAttributes', (node) => {
  if (node.tagName === 'A' && node.hasAttribute('href')) {
    node.setAttribute('target', '_blank');
    node.setAttribute('rel', 'noopener noreferrer');
  }
});

const clean = DOMPurify.sanitize(dirty);

Hooks are global to the instance and stack up, so register them once at module load. Use removeHook or removeAllHooks in tests, otherwise hooks leak between cases.

Get a DOM fragment instead of a stringreturn-dom-fragment

const fragment = DOMPurify.sanitize(dirty, {
  RETURN_DOM_FRAGMENT: true,
});

container.replaceChildren(fragment);

Skipping the string round trip removes a step where post-processing could reintroduce a payload, and it avoids a second parse of markup that was just parsed.

Back a Trusted Types policy with DOMPurifytrusted-types-policy

window.trustedTypes.createPolicy('my-organization', {
  createHTML: (input) =>
    DOMPurify.sanitize(input, { TRUSTED_TYPES_POLICY: null }),
});

createHTML must receive a plain string, so keep RETURN_TRUSTED_TYPE off here. TRUSTED_TYPES_POLICY: null stops DOMPurify creating its own internal dompurify policy, which a strict trusted-types CSP would block anyway. Never hand your own wrapping policy back to DOMPurify: that recursion throws by design.

Set config once for the whole apppersistent-config

DOMPurify.setConfig({
  USE_PROFILES: { html: true },
  ALLOWED_ATTR: ['href', 'title', 'class'],
});

DOMPurify.sanitize(dirty); // uses the persisted config

// later, back to defaults
DOMPurify.clearConfig();

There is only one active persistent config, and while it is set, config objects passed to sanitize() are ignored entirely. That silent override surprises people who mix both styles.

Extend the allow-list for your own markupallow-custom-elements

const clean = DOMPurify.sanitize(dirty, {
  ADD_TAGS: ['my-widget'],
  ADD_ATTR: ['data-widget-id'],
});

ADD_TAGS and ADD_ATTR extend the defaults rather than replacing them. Read the wiki list of attributes to think twice about first: allowing style, srcset, or anything URL-shaped widens the attack surface in non-obvious ways.

Control which URL schemes surviveuri-protocols

const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_URI_REGEXP:
    /^(?:(?:https?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
});

The default already blocks javascript: and data: in most positions while permitting http, https, ftp, mailto, tel, callto, sms, cid, xmpp, and matrix. Narrowing it is fine; setting ALLOW_UNKNOWN_PROTOCOLS: true to make one link type work re-opens the hole.

See what got stripped while debugginginspect-removed

const clean = DOMPurify.sanitize(dirty);
console.log(DOMPurify.removed); // [{ element: ... }, { attribute: ... }]

The README is explicit that this is a debugging aid only. Do not branch security decisions such as rejecting a submission on the contents of DOMPurify.removed.

Alternatives

PackageRegistryPick it when
sanitize-htmlnpmYou sanitize on a Node server and would rather have a parser-based library built for that job than run jsdom to fake a browser
isomorphic-dompurifynpmYou need the same sanitizer call to work in the browser and in server rendering without writing the jsdom wiring yourself
xssnpmYou want a small allow-list filter with no DOM requirement and can accept weaker coverage of parser edge cases