GitHub Advisoryadvisory · npm
GHSA-2v35-w6hq-6mfw xmldom: Uncontrolled recursion in XML serialization leads to DoS
HIGH
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
- https://github.com/xmldom/xmldom/security/advisories/GHSA-2v35-w6hq-6mfw web
- https://nvd.nist.gov/vuln/detail/CVE-2026-41673 advisory
- https://github.com/xmldom/xmldom/commit/17678a2a73ecbd1a2da90f3d47dc23da9cef81aa web
- https://github.com/xmldom/xmldom/commit/291257493cb0eb6980eda83b162a9c4e6d7d2597 web
- https://github.com/xmldom/xmldom/commit/2d6d6916ed8a4c223db1f6d7560ab4544c465b0f web
- https://github.com/xmldom/xmldom/commit/430357c7b6333108856e917bf2367afe5ceb6f8a web
- https://github.com/xmldom/xmldom/commit/4845ef109221df0890825de2822fbe77afba3afe web
- https://github.com/xmldom/xmldom/commit/8834218c85ac2a4d757b9587c9028e67c2f7b6c3 web
- https://github.com/xmldom/xmldom/commit/8b7cfd1491314abdc347261921d7334ff15f7112 web
- https://github.com/xmldom/xmldom/commit/b0620383abc1df067f3ce1014c43ae1bc1161eeb web
- https://github.com/xmldom/xmldom/commit/e6edcab6bef5bcdba0b220bb35442aa72f452b84 web
- https://github.com/xmldom/xmldom package
- https://github.com/xmldom/xmldom/releases/tag/0.8.13 web
- https://github.com/xmldom/xmldom/releases/tag/0.9.10 web