GHSA-c9rx-x947-fp3h: Medium severity npm/sanitize-html vulnerability

Published Oct 21, 2026
·
Updated

Summary

Since 2.18.0, when an <iframe> is not allowed and disallowedTagsMode is 'discard' (the defaults), its raw-text content is re-sanitized by a recursive call to sanitizeHtml(text, options) (the change from #5564, fixing #5550). The recursion has no bound and starts each nested call with a fresh state. Two consequences, both absent from 2.17.7 and 2.17.4:

1. A few kilobytes of unclosed <iframe> tags throw RangeError: Maximum call stack size exceeded with the default options. Any application that sanitizes untrusted HTML with 2.18.0 can be made to throw on a small crafted input. 2. nestingLimit is not enforced across a discarded iframe: each nested iframe restarts the depth count at zero, so the output can be nested arbitrarily deep regardless of the configured limit.

Affected versions

- 2.18.0 (the current latest), and the main branch at the time of writing (the recursive call is still in packages/sanitize-html/index.js, in the ontext handler, branch tag === 'iframe' && !tagAllowed(tag) && options.disallowedTagsMode === 'discard'). - Not affected: 2.17.7 and 2.17.4 (tested with the same inputs below).

Tested with Node.js v26.7.0.

Reproduction 1: stack overflow with the default options

js const sanitizeHtml = require('sanitize-html');

// with 2.18.0: sanitizeHtml('<iframe>'.repeat(1000)); // 8,000 chars: returns '' sanitizeHtml('<iframe>'.repeat(2000)); // 16,000 chars: throws RangeError: Maximum call stack size exceeded sanitizeHtml('<iframe>'.repeat(20000)); // 160,000 chars: throws RangeError: Maximum call stack size exceeded

Results:

| input | 2.17.4 | 2.17.7 | 2.18.0 | |---|---|---|---| | '<iframe>'.repeat(1000) | ok | ok | ok | | '<iframe>'.repeat(2000) | ok | ok | RangeError | | '<iframe>'.repeat(20000) | ok | ok | RangeError |

The threshold depends on the remaining stack, so it is lower when sanitizeHtml is called from an already deep call stack (for example inside a server-side rendering pass).

Reproduction 2: nestingLimit is bypassed

js const sanitizeHtml = require('sanitize-html');

const input = ('<iframe>' + '<b>'.repeat(3)).repeat(50) + 'x'; const out = sanitizeHtml(input, { allowedTags: ['b'], nestingLimit: 3 }); // maximum nesting depth of <b> in out: // 2.17.4: 2 2.17.7: 0 (fallback escaped as text) 2.18.0: 150

Without an iframe, the same options cap the output at three nested <b> in all three versions. With k repetitions of '<iframe>' + '<b>'.repeat(3), 2.18.0 returns 3 k nested <b> (measured for k = 1, 5, 50: depth 3, 15, 150).

Cause

In 2.18.0 the ontext handler does:

js } else if ( tag === 'iframe' && !tagAllowed(tag) && options.disallowedTagsMode === 'discard' ) { result += sanitizeHtml(text, options); }

Because htmlparser2 treats <iframe> as a raw-text element, the text of an unclosed iframe is the whole remainder of the input; re-parsing it can meet another unclosed <iframe>, and so on, one stack frame (and one full re-parse) per iframe. The nested call also starts with its own depth, so tags inside the fallback are not counted against nestingLimit together with the enclosing tags.

Possible directions

Not a patch, only what would close both effects: bound the recursion (or process the fallback without recursing), and carry the current depth into the nested sanitization so that nestingLimit applies to the whole output.

Affected Software

1 affected componentFixes available
npm/sanitize-html>=2.18.0<2.18.1
2.18.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/sanitize-html to a version that resolves this vulnerability.

    Fixed in 2.18.1
  2. Upgrade

    Upgrade sanitize-html to a version that resolves this vulnerability.

    Fixed in 2.17.7

Event History

Oct 21, 2026
Advisory Published
via GitHub·05:05 PM
Data Sourced
via GitHub·05:05 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Are applications using the default sanitizer configuration exposed?

Yes. In version 2.18.0, iframe is not allowed and disallowedTagsMode is set to discard by default, which triggers the vulnerable recursive handling.

2

What does an attacker need to exploit the stack-exhaustion issue?

They need to be able to supply HTML that the application passes to sanitize-html. A small crafted input containing a few kilobytes of unclosed iframe tags can cause sanitization to throw RangeError: Maximum call stack size exceeded.

3

Does configuring nestingLimit prevent the issue?

Not when nested content is processed through a discarded iframe. Each recursive sanitization call resets the depth count, allowing output to be nested arbitrarily deeply despite the configured nestingLimit.

4

How can I determine whether my deployment is affected?

Version 2.18.0 and the main branch at the time of the advisory are affected when an iframe is disallowed and disallowedTagsMode is discard. Versions 2.17.7 and 2.17.4 were tested with the same inputs and were not affected.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203