GHSA-c9rx-x947-fp3h

Suggest an improvement
Source
https://github.com/advisories/GHSA-c9rx-x947-fp3h
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-c9rx-x947-fp3h/GHSA-c9rx-x947-fp3h.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-c9rx-x947-fp3h
Aliases
Downstream
CGA (2)
Published
2026-10-21T17:05:01Z
Modified
2026-10-21T17:15:08Z
Severity
  • 5.3 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L CVSS Calculator
Summary
ApostropheCMS sanitize-html: unbounded recursion when re-sanitizing the fallback of a discarded iframe (stack overflow, nestingLimit not enforced)
Details

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:

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

Database specific
{
    "cwe_ids":  [
        "CWE-674"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-10-21T17:05:01Z",
    "nvd_published_at":  "2026-10-20T17:17:35Z",
    "severity":  "MODERATE"
}
References

Affected packages

npm / sanitize-html

Package

Name
sanitize-html
View open source insights on deps.dev
Purl
pkg:npm/sanitize-html

Affected ranges

Type
SEMVER
Events
Introduced
2.18.0
Fixed
2.18.1

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-c9rx-x947-fp3h/GHSA-c9rx-x947-fp3h.json"