ApostropheCMS sanitize-html: unbounded recursion when re-sanitizing the fallback of a discarded iframe (stack overflow, nestingLimit not enforced)
What fixes it
- 2.18.1sanitize-html, npmGitHub
npm install sanitize-html@2.18.1
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
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:
- A few kilobytes of unclosed
<iframe>tags throwRangeError: 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.
nestingLimitis 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 themainbranch 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
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 exceededResults:
| 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
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: 150Without 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:
} 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.
Text from GitHub Advisory Database
Affected versions 4
- sanitize-html, npm>= 2.18.0, < 2.18.1GitHub
- apostropheaffected: < 4.34.0CVE List
- sanitize-htmlaffected: >= 2.18.0, < 2.18.1CVE List
- sanitize-html, npmfrom 2.18.0, fixed in 2.18.1OSV
References 6
- github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498Fixvia CVE List
- github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5eaFixvia CVE List
- github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.1Fixvia CVE List
- github.com/apostrophecms/apostrophe/security/advisories/GHSA-c9rx-x947-fp3hAdvisoryvia CVE List
- github.com/advisories/GHSA-c9rx-x947-fp3hAdvisoryvia GitHub
- nvd.nist.gov/vuln/detail/CVE-2026-113650Referencevia GitHub