mrkeyoor.com_
Thu 06 Aug 13:51 UTC
npmSecurityupdated 06 Aug 2026

sanitize-html

sanitize-html takes a string of untrusted HTML and returns a string containing only the tags, attributes, classes, styles and URL schemes you explicitly allowed. It runs on htmlparser2 rather than a DOM, so it works in plain Node with no jsdom and no browser, which is what makes it the usual server-side answer for rich text coming out of CKEditor, TinyMCE or a Markdown pipeline. Everything it keeps gets escaped properly, disallowed tags are dropped while their text is preserved by default, href and src values are checked against an allowed scheme list, and inline styles are parsed with postcss so you can allow specific CSS properties by regular expression. It was written for ApostropheCMS and now lives inside that project's monorepo.

Verdict

The pragmatic default for cleaning rich text on a Node server, with the most detailed per-tag policy options of anything in this space. Stay close to the default allowlist, subscribe to its advisories, and reach for DOMPurify instead the moment the sanitizing needs to happen in a browser.

API stability4/5The sanitizeHtml(dirty, options) signature has not changed since 2.0 and new options are additive, but 2.17.6 raised the Node engines floor to 22.12.0 inside a patch release, which is a breaking install change hiding behind a security fix
Docs3/5The README covers every option with a runnable example and warns clearly about the risky ones, but it is a single long page with no API reference, it still claims Node 10+ support against an engines field of >=22.12.0, and its allowed-schemes section disagrees with the defaults printed higher up and with the source
Maintenance4/5Six releases between February and July 2026, each security report acknowledged and fixed with a detailed changelog entry, and a CHANGELOG that explains the actual parser behaviour behind each bypass; the numbers on GitHub (4.6k stars, 120 open issues, 134 including PRs) belong to the whole apostrophe monorepo it now lives in, not to this package
Ecosystem5/5About 12.8M downloads a week, a de facto standard in Node CMS and comment pipelines, with community TypeScript types on DefinitelyTyped and wrappers in most server frameworks

Use it if

  • You accept rich text from users on a Node server and need to store a cleaned version, which is the only place sanitizing actually counts because a browser can never be trusted to have done it
  • You need per-tag control rather than a fixed policy: allowedAttributes keyed by tag, allowedClasses with prefix wildcards or regexes, allowedStyles as CSS property regexes, and allowedSchemesByTag
  • You want to rewrite as well as strip: transformTags can turn ol into ul or inject rel="nofollow" on every anchor, and exclusiveFilter can drop empty links while textFilter rewrites text nodes
  • You are pasting from Word or another editor and want the pile of junk markup and inline CSS reduced to something your templates can render, which is the case the library was originally built for
Skip it if

Setup reality

npm install sanitize-html pulls seven pure JavaScript dependencies, postcss and htmlparser2 among them, with no native build. The package is CommonJS with a single default export, so under TypeScript you need esModuleInterop enabled or the import * as sanitizeHtml form, plus npm install -D @types/sanitize-html because no types ship. Since 2.17.6 the engines field requires Node >= 22.12.0, which is the first 22.x that can require() an ES module, and installers with engine-strict will refuse older runtimes. There is no prebuilt browser bundle any more; 2.x expects you to run it through your own bundler, and in the browser you may have to set parseStyleAttributes: false because of an open postcss issue. Watch the defaults too: the default allowedTags list is generous but excludes img and iframe, and the source default allowedSchemes is http, https, ftp, mailto and tel even though one section of the README still lists only four.

Patterns

Clean a string with the built-in allowlistsanitize-with-defaults

import sanitizeHtml from 'sanitize-html';

const clean = sanitizeHtml(dirty);
// <script>alert(1)</script> -> ''
// <img src=x onerror=alert(1)> -> ''

The default allowlist covers headings, lists, tables, links and inline formatting but not img, iframe, script or style. Comments are always dropped and all text is escaped, so ampersands come back as &amp;.

Define a tight policy of your owncustom-allowlist

const clean = sanitizeHtml(dirty, {
  allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  allowedAttributes: {
    a: ['href', 'title', 'target', 'rel']
  },
  allowedSchemes: ['http', 'https', 'mailto']
});

Passing allowedTags replaces the default list entirely rather than adding to it. If you want no tags at all you must pass allowedTags: [] and allowedAttributes: {}, because omitting them means the defaults apply.

Add one tag to the default setextend-defaults

const clean = sanitizeHtml(dirty, {
  allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
  allowedAttributes: {
    ...sanitizeHtml.defaults.allowedAttributes,
    img: ['src', 'alt', 'width', 'height', 'loading']
  }
});

sanitizeHtml.defaults exposes the built-in configuration so you can extend instead of retyping it. Spread allowedAttributes rather than assigning it, or you drop the default a: ['href', 'name', 'target'] entry along the way.

Reduce HTML to plain textstrip-all-markup

const text = sanitizeHtml(dirty, {
  allowedTags: [],
  allowedAttributes: {},
  disallowedTagsMode: 'discard'
});

With discard the tags go and the text inside them stays. Use completelyDiscard instead if you want the contents of disallowed tags thrown away too. Neither inserts whitespace, so <p>a</p><p>b</p> becomes ab.

Permit specific CSS classes and inline stylesallowed-classes-and-styles

const clean = sanitizeHtml(dirty, {
  allowedTags: ['p', 'code', 'span'],
  allowedAttributes: {
    p: ['style'],
    span: ['class']
  },
  allowedClasses: {
    code: ['language-*', 'lang-*'],
    '*': ['highlight']
  },
  allowedStyles: {
    '*': {
      color: [/^#(0x)?[0-9a-f]+$/i],
      'text-align': [/^left$/, /^right$/, /^center$/],
      'font-size': [/^\d+(?:px|em|%)$/]
    }
  }
});

allowedStyles never runs unless style is also listed in allowedAttributes for that tag. Anchor every regex with ^ and $: a pattern like /red/ matches url(javascript:...) red too. allowedClasses implies the class attribute is allowed on that tag.

Allow embeds from named hosts onlyiframe-allowlist

const clean = sanitizeHtml(dirty, {
  allowedTags: ['p', 'iframe'],
  allowedAttributes: {
    iframe: ['src', 'width', 'height', 'allowfullscreen']
  },
  allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
  allowedIframeDomains: ['zoom.us'],
  allowIframeRelativeUrls: false
});

Hostnames must match exactly, so www.youtube.com does not cover youtube.com. allowedIframeDomains matches any subdomain depth. A src that fails the check is stripped and you are left with an empty iframe element, not a removed one.

Rewrite tags and force attributestransform-tags

const clean = sanitizeHtml(dirty, {
  allowedTags: ['a', 'ul', 'li'],
  allowedAttributes: { a: ['href', 'target', 'rel'] },
  transformTags: {
    ol: 'ul',
    a: (tagName, attribs) => ({
      tagName: 'a',
      attribs: { ...attribs, target: '_blank', rel: 'noopener noreferrer' }
    })
  }
});

Attributes you add in a transform still have to be in allowedAttributes or they are stripped straight back out. sanitizeHtml.simpleTransform('ul', {class: 'foo'}) covers the common case and merges with existing attributes unless you pass false as the third argument.

Drop empty elements and rewrite text nodesfilter-tags-and-text

const clean = sanitizeHtml(dirty, {
  exclusiveFilter: frame =>
    frame.tag === 'a' && !frame.attribs.href ? 'excludeTag' : frame.tag === 'p' && !frame.text.trim(),
  textFilter: (text, tagName) => (tagName === 'a' ? text : text.replace(/\.\.\./g, '&hellip;'))
});

Returning true from exclusiveFilter removes the tag and everything in it; returning the string 'excludeTag' removes only the tag and keeps the content. The text handed to textFilter is already escaped, so returning raw < there reopens a hole you closed.

Show disallowed markup instead of deleting itescape-instead-of-discard

// <disallowed>content</disallowed> -> &lt;disallowed&gt;content&lt;/disallowed&gt;
const clean = sanitizeHtml(dirty, {
  allowedTags: ['b', 'i'],
  disallowedTagsMode: 'escape',
  preserveEscapedAttributes: true
});

The four modes are discard (default), completelyDiscard, escape and recursiveEscape. recursiveEscape escapes nested allowed tags as well. Attributes are dropped when escaping unless you set preserveEscapedAttributes, added in 2.17.0.

Allow attribute prefixes and global attributeswildcard-attributes

const clean = sanitizeHtml(dirty, {
  allowedAttributes: {
    a: ['href', 'data-*'],
    '*': ['id', 'lang', 'dir'],
    iframe: [
      { name: 'sandbox', multiple: true, values: ['allow-scripts', 'allow-same-origin'] }
    ]
  }
});

The '*' tag key applies the listed attributes to every allowed tag, and data-* allows any attribute with that prefix. The object form restricts an attribute to specific values; with multiple: true several space-separated values may pass, otherwise the value must match exactly one.

Guard against nesting bombs and stray editor outputlimit-depth-and-boundary

const clean = sanitizeHtml(dirty, {
  nestingLimit: 10,
  enforceHtmlBoundary: true,
  nonTextTags: ['style', 'script', 'textarea', 'option', 'xmp', 'noscript']
});

nestingLimit strips tags deeper than the limit as if they were disallowed, which blunts pathological inputs from pasted documents. enforceHtmlBoundary throws away everything outside <html></html>. If you override nonTextTags you own the whole list, so keep style and script in it.

Use it from TypeScripttypescript-setup

// npm install sanitize-html
// npm install -D @types/sanitize-html

import sanitizeHtml from 'sanitize-html'; // needs esModuleInterop: true
// otherwise: import * as sanitizeHtml from 'sanitize-html';

const options: sanitizeHtml.IOptions = {
  allowedTags: ['b', 'i', 'a'],
  allowedAttributes: { a: ['href'] }
};

export const clean = (dirty: string): string => sanitizeHtml(dirty, options);

The package itself is untyped and the maintainers say that will not change, so the types come from DefinitelyTyped and lag new options. TypeScript 4.5 is the floor because of the htmlparser2 types the definitions depend on.

Alternatives

PackageRegistryPick it when
dompurifynpmYou are sanitizing in the browser or already run jsdom on the server and want a real DOM parser doing the work
xssnpmYou want a smaller dependency-light allowlist filter for Node and can live with a narrower option surface
hast-util-sanitizenpmYour content already runs through unified, remark or rehype and you would rather sanitize the syntax tree than a string