GitHub Advisoryadvisory · npm

GHSA-2v35-w6hq-6mfw xmldom: Uncontrolled recursion in XML serialization leads to DoS

HIGH Published 2026-04-22 Modified 2026-05-08 Official page ↗
Aliases
CVE-2026-41673
Ecosystem
npm
Packages
@xmldom/xmldom, xmldom
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
Weaknesses
CWE-674
Reviewed
2026-04-22

Affected packages

• npm @xmldom/xmldom: < 0.8.13 → fixed in 0.8.13 • npm @xmldom/xmldom: >= 0.9.0, < 0.9.10 → fixed in 0.9.10 • npm xmldom: <= 0.6.0 (no fix yet)

Description

Summary Seven recursive traversals in `lib/dom.js` operate without a depth limit. A sufficiently deeply nested DOM tree causes a `RangeError: Maximum call stack size exceeded`, crashing the application. Reported operations: • `Node.prototype.normalize()` — reported by @praveen-kv (email 2026-04-05) and @KarimTantawey (GHSA-fwmp-8wwc-qhv6, via `DOMParser.parseFromString()`) • `XMLSerializer.serializeToString()` — reported by @Jvr2022 (GHSA-2v35-w6hq-6mfw) and @KarimTantawey (GHSA-j2hf-fqwf-rrjf) Additionally, discovered in research: • `Element.getElementsByTagName()` / `getElementsByTagNameNS()` / `getElementsByClassName()` / `getElementById()` • `Node.cloneNode(true)` • `Document.importNode(node, true)` • `node.textContent` (getter) • `Node.isEqualNode(other)` All seven share the same root cause: pure-JavaScript recursive tree traversal with no depth guard. A single deeply nested document (parsed successfully) triggers any or all of these operations. --- Details Root cause `lib/dom.js` implements DOM tree traversals as depth-first recursive functions. Each level of element nesting adds one JavaScript call frame. The JS engine's call stack is finite; once exhausted, a `RangeError: Maximum call stack size exceeded` is thrown. This error may not be caught reliably at stack-exhaustion depths because the catch handler itself requires stack frames to execute — especially in async scenarios, where an uncaught `RangeError` inside a callback or promise chain can crash the entire Node.js process. Parsing a deeply nested document succeeds — the SAX parser in `lib/sax.js` is iterative. The crash occurs during subsequent operations on the parsed DOM. `Node.prototype.normalize()` — reported by @praveen-kv `lib/dom.js:1296–1308` (main): ```js normalize: function () { var child = this.firstChild; while (child) { var next = child.nextSibling; if (next && next.nodeType == TEXT_NODE && child.nodeType == TEXT_NODE) { this.removeChild(next); child.appendData(next.data); } else { child.normalize(); // recursive call — no depth guard child = next; } } }, ``` Crash threshold (Node.js 18, default stack): ~10,000 levels. `XMLSerializer.serializeToString()` — reported by @Jvr2022 `lib/dom.js:2790–2974` (main): The internal `serializeToString` worker recurses into child nodes at four call sites, each passing a `visibleNamespaces.slice()` copy. The per-frame allocation causes earlier stack exhaustion than `normalize()`. Crash threshold (Node.js 18, default stack): ~5,000 levels. Additional recursive entry points All five crash at ~10,000 levels on Node.js 18. | Function | Definition | Public API entry point(s) | Crash depth (Node.js 18) | |-----------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------------------------| | `_visitNode` | `lib/dom.js:1529` | `getElementsByTagName()`, `getElementsByTagNameNS()`, `getElementsByClassName()`, `getElementById()` | ~10,000 levels | | `cloneNode` (module fn) | `lib/dom.js:3037` | `Node.prototype.cloneNode(true)` | ~10,000 levels | | `importNode` (module fn) | `lib/dom.js:2975` | `Document.prototype.importNode(node, true)` | ~10,000 levels | | `getTextContent` (inner fn) | `lib/dom.js:3130` | `node.textContent` (getter) | ~10,000 levels | | `isEqualNode` | `lib/dom.js:1120` | `Node.prototype.isEqualNode(other)` | ~10,000 levels | Both active branches (`main` and `release-0.8.x`) are identically affected. The unscoped `xmldom` package (≤ 0.6.0) carries the same recursive patterns from its initial commit. Browser behavior Tested with Chromium 147 (Playwright headless). Chromium's native C++ implementations of all seven DOM methods are iterative — they traverse the DOM without consuming JS call stack frames. All seven succeed at depths up to 20,000 without any crash. When `@xmldom/xmldom` is bundled and run in a browser context the same recursive JS code executes under the browser's V8 stack limit (~12,000–13,000 frames). The crash thresholds are similar to those observed on Node.js 18 (~5,000 for `serializeToString`, ~10,000 for the remaining six). The vulnerability is specific to xmldom's pure-JavaScript recursive implementation, not an inherent property of the DOM operations. --- PoC `normalize()` (from @praveen-kv report, 2026-04-05) ```js const { DOMParser } = require('@xmldom/xmldom'); function generateNestedXML(depth) { return '' + ''.repeat(depth) + 'text' + ''.repeat(depth) + ''; } const doc = new DOMParser().parseFromString(generateNestedXML(10000), 'text/xml'); doc.documentElement.normalize(); // RangeError: Maximum call stack size exceeded ``` `XMLSerializer.serializeToString()` (from GHSA-2v35-w6hq-6mfw) ```js const { DOMParser, XMLSerializer } = require('@xmldom/xmldom'); const depth = 5000; const xml = ''.repeat(depth) + ''.repeat(depth); const doc = new DOMParser().parseFromString(xml, 'text/xml'); new XMLSerializer().serializeToString(doc); // RangeError: Maximum call stack size exceeded ``` The other methods have been verified using similar pocs. --- Impact Any service that accepts attacker-controlled XML and subsequently calls any of the seven affected DOM operations can be forced into a reliable denial of service with a single crafted payload. The immediate result is an uncaught `RangeError` and failed request processing. In deployments where uncaught exceptions terminate the worker or process, the impact can extend beyond a single request and disrupt service availability more broadly. No authentication, special options, or invalid XML is required. A valid, deeply nested XML document is enough. --- Disclosure The `normalize()` vector was publicly disclosed at 2026-04-06T11:25:07Z via xmldom/xmldom#987 (closed without merge). `serializeToString()` and the five additional recursive entry points were not mentioned in that PR. --- Fix Applied All seven affected traversals have been converted from recursive to iterative implementations, eliminating call-stack consumption on deep trees. `walkDOM` utility A new `walkDOM(node, context, callbacks)` utility is introduced. It traverses the subtree rooted at `node` in depth-first order using an explicit JavaScript array as a stack, consuming heap memory instead of call-stack frames. `context` is an arbitrary value threaded through the walk — each `callbacks.enter(node, context)` call returns the context to pass to that node's chi…

Related

Alias · CVE (1)

  • CVE-2026-41673 xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) `DOMParser` and `XMLSerializer` module. vulnerability

Weakness · CWE (1)

  • CWE-674 Uncontrolled Recursion weakness

References