Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

CVE-2026-113650 — sanitize-html

MEDIUMFix: apostrophecms/apostrophe@06e4b37

CVE-2026-113650 is a medium-severity (CVSS 5.3) CWE-674 vulnerability in sanitize-html. A fix is available for sanitize-html — see the affected versions and patch details below.

ApostropheCMS sanitize-html: unbounded recursion when re-sanitizing the fallback of a discarded iframe (stack overflow, nestingLimit not enforced)

Also known asGHSA-c9rx-x947-fp3h
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 22, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs22th percentile — riskier than 22% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-113650 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,415 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
📦sanitize-html

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects npm packages — download data is not available via public APIs for these ecosystems.

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:

  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

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:

input2.17.42.17.72.18.0
'<iframe>'.repeat(1000)okokok
'<iframe>'.repeat(2000)okokRangeError
'<iframe>'.repeat(20000)okokRangeError

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: 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:

} 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 Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmsanitize-html≥ 2.18.0&&< 2.18.12.18.1npm install sanitize-html@2.18.1

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for sanitize-html, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update sanitize-html to 2.18.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-113650 is resolved across your whole dependency graph.

  3. Workarounds

    Stop feeding it untrusted input: reject or quarantine files and payloads from unverified sources until you can upgrade, restrict accepted formats to the ones you actually need, and run the parsing or decoding step in a least-privileged sandbox or short-lived worker so a crash or corrupted read cannot reach the rest of the process.

Frequently Asked Questions

### 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 cr
O3 Security · Impact-Aware SCA

Is CVE-2026-113650 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-113650: sanitize-html — Fixed in 2.18.1