Severity by source
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Primary rating from Vendor (https://github.com/xmldom/xmldom).
CVSS VectorVendor: https://github.com/xmldom/xmldom
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Lifecycle Timeline
8Blast Radius
ecosystem impact- 4 npm packages depend on @xmldom/xmldom (4 direct, 0 indirect)
- 1,984 npm packages depend on xmldom (850 direct, 1,160 indirect)
Ecosystem-wide dependent count for version 0.9.0 and other introduced versions.
DescriptionCVE.org
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, viaDOMParser.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):
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)
const { DOMParser } = require('@xmldom/xmldom');
function generateNestedXML(depth) {
return '<root>' + '<a>'.repeat(depth) + 'text' + '</a>'.repeat(depth) + '</root>';
}
const doc = new DOMParser().parseFromString(generateNestedXML(10000), 'text/xml');
doc.documentElement.normalize();
// RangeError: Maximum call stack size exceededXMLSerializer.serializeToString() (from GHSA-2v35-w6hq-6mfw)
const { DOMParser, XMLSerializer } = require('@xmldom/xmldom');
const depth = 5000;
const xml = '<a>'.repeat(depth) + '</a>'.repeat(depth);
const doc = new DOMParser().parseFromString(xml, 'text/xml');
new XMLSerializer().serializeToString(doc);
// RangeError: Maximum call stack size exceededThe 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 children, enabling per-branch state (e.g. namespace snapshots in the serializer). callbacks.exit(node, context) (optional) is called in post-order after all children have been visited.
The following six operations are re-implemented on top of walkDOM:
| Operation | Public entry point(s) |
|---|---|
_visitNode helper | getElementsByTagName(), getElementsByTagNameNS(), getElementsByClassName(), getElementById() |
getTextContent inner function | node.textContent getter |
cloneNode module function | Node.prototype.cloneNode(true) |
importNode module function | Document.prototype.importNode(node, true) |
serializeToString worker | XMLSerializer.prototype.serializeToString(), Node.prototype.toString(), NodeList.prototype.toString() |
normalize | Node.prototype.normalize() |
normalize uses walkDOM with a null context and an enter callback that merges adjacent Text children of the current node before walkDOM reads and queues those children - so the surviving post-merge children are what the walker descends into.
Custom iterative loop for isEqualNode
One function cannot use walkDOM:
Node.prototype.isEqualNode(other) (0.9.x only; absent from 0.8.x) compares two trees in parallel. It maintains an explicit stack of {node, other} node pairs - one node from each tree - which cannot be expressed with walkDOM's single-tree visitor.
After the fix
All seven entry points succeed on trees of arbitrary depth without throwing RangeError. The original PoCs still demonstrate the vulnerability on unpatched versions and confirm the fix on patched versions.
AnalysisAI
Denial of service in @xmldom/xmldom Node.js XML library allows remote attackers to crash applications via deeply nested XML documents. Seven DOM traversal methods (normalize, serializeToString, getElementsByTagName, cloneNode, importNode, textContent getter, isEqualNode) implement unbounded recursion consuming call stack frames until RangeError exception terminates the process. Exploitation requires no authentication - attackers send a single valid XML payload nested ~5,000-10,000 levels deep to trigger stack exhaustion in any subsequent DOM operation. Browser implementations of identical DOM methods use iterative C++ code and are unaffected. CVSS 8.7 High severity reflects network attack vector with no complexity barriers. Vendor-released patches (0.8.13, 0.9.10) replace all recursive traversals with iterative 'walkDOM' utility consuming heap instead of stack. Legacy unscoped 'xmldom' package (≤0.6.0) remains unfixed.
Technical ContextAI
@xmldom/xmldom is a pure-JavaScript implementation of W3C DOM Level 2 Core for Node.js environments, providing XML parsing and manipulation when native browser DOM APIs are unavailable. The library consists of two components: an iterative SAX parser (lib/sax.js) that successfully parses arbitrarily deep XML without stack consumption, and DOM tree manipulation functions (lib/dom.js) implemented as depth-first recursive functions. Each level of XML nesting adds one JavaScript call frame; V8's default stack limit (~10,000-13,000 frames in Node.js 18) creates a hard ceiling. CWE-674 (Uncontrolled Recursion) is the root cause - seven distinct tree-walking functions (normalize, serializeToString via internal worker, _visitNode helper used by four getElementsBy* variants, cloneNode/importNode module functions, getTextContent, isEqualNode) lack depth guards or iterative fallback logic. The vulnerability manifests only in JavaScript execution contexts; Chromium's native C++ DOM implementations of identical methods (tested at 20,000+ depth) traverse iteratively. CPE data identifies three affected package namespaces: scoped @xmldom/xmldom (primary, actively maintained), legacy unscoped xmldom (abandoned ≤0.6.0), and NPM ecosystem exposure through transitive dependencies in projects consuming either namespace.
RemediationAI
Upgrade to patched versions: @xmldom/xmldom 0.8.13 or later for 0.8.x users, or 0.9.10 or later for 0.9.x users, available from https://github.com/xmldom/xmldom/releases/tag/0.8.13 and https://github.com/xmldom/xmldom/releases/tag/0.9.10. Ten GitHub commits (17678a2, 291257, 2d6d691, 430357c, 4845ef1, 8834218, 8b7cfd1, b062038, e6edcab listed in advisory references) collectively rewrite all seven recursive traversals as iterative implementations using a new walkDOM utility function that maintains an explicit JavaScript array stack instead of consuming call frames, eliminating depth limits. For environments unable to immediately upgrade, implement XML depth validation before parsing: reject documents where maximum element nesting exceeds 1,000 levels using a streaming pre-parse check or SAX parser with depth counter (note: @xmldom/xmldom's own SAX parser does not expose depth to applications, requiring external validation). Compensating control trade-off: depth limits below 1,000 may break legitimate deeply-nested documents in specialized domains (academic XML corpora, auto-generated config files). Do NOT rely on try-catch wrappers around DOM operations - RangeError at stack exhaustion may not be catchable in async code paths and catch handlers themselves require stack frames. For legacy xmldom users (≤0.6.0) with no patch available: migrate to @xmldom/xmldom 0.8.13+ or replace with alternative maintained libraries (jsdom, libxmljs2) that use native/iterative DOM implementations. Container-level mitigation: configure Node.js --stack-size parameter to crash earlier with smaller payloads, providing faster failure detection but not preventing DoS.
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se
Same weakness CWE-674 – Uncontrolled Recursion
View allSame technique Denial Of Service
View allVendor StatusVendor
SUSE
Severity: High| Product | Status |
|---|---|
| SUSE Linux Enterprise Desktop 15 SP7 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP7 | Fixed |
| SUSE Linux Enterprise Module for Python 3 15 SP7 | Fixed |
| SUSE Linux Enterprise Server 15 SP7 | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP7 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP4 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP4-LTSS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP5 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP5-LTSS | Fixed |
| SUSE Linux Enterprise Module for Python 3 15 SP6 | Fixed |
| SUSE Linux Enterprise Module for Server Applications 15 SP4 | Fixed |
| SUSE Linux Enterprise Module for Server Applications 15 SP5 | Fixed |
| SUSE Linux Enterprise Server 15 SP4 | Fixed |
| SUSE Linux Enterprise Server 15 SP4-LTSS | Fixed |
| SUSE Linux Enterprise Server 15 SP5 | Fixed |
| SUSE Linux Enterprise Server 15 SP5-LTSS | Fixed |
| SUSE Linux Enterprise Server 15 SP6 | Fixed |
| SUSE Linux Enterprise Server 15 SP6-LTSS | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP6 | Fixed |
| SUSE Manager Proxy 4.3 | Fixed |
| SUSE Manager Proxy LTS 4.3 | Fixed |
| SUSE Manager Retail Branch Server 4.3 | Fixed |
| SUSE Manager Retail Branch Server LTS 4.3 | Fixed |
| SUSE Manager Server 4.3 | Fixed |
| SUSE Manager Server LTS 4.3 | Fixed |
| SUSE CaaS Platform 4.0 | Fixed |
| SUSE Enterprise Storage 6 | Fixed |
| SUSE Enterprise Storage 7 | Fixed |
| SUSE Enterprise Storage 7.1 | Fixed |
| SUSE Linux Enterprise Desktop 15 SP6 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP1 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP1-ESPOS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP1-LTSS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP2 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP2-ESPOS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP2-LTSS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP3 | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP3-ESPOS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP3-LTSS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP4-ESPOS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP5-ESPOS | Fixed |
| SUSE Linux Enterprise High Performance Computing 15 SP6 | Fixed |
| SUSE Linux Enterprise Module for Server Applications 15 SP1 | Fixed |
| SUSE Linux Enterprise Module for Server Applications 15 SP2 | Fixed |
| SUSE Linux Enterprise Module for Server Applications 15 SP3 | Fixed |
| SUSE Linux Enterprise Real Time 15 SP2 | Fixed |
| SUSE Linux Enterprise Real Time 15 SP3 | Fixed |
| SUSE Linux Enterprise Real Time 15 SP4 | Fixed |
| SUSE Linux Enterprise Server 15 SP1 | Fixed |
| SUSE Linux Enterprise Server 15 SP1-BCL | Fixed |
| SUSE Linux Enterprise Server 15 SP1-LTSS | Fixed |
| SUSE Linux Enterprise Server 15 SP2 | Fixed |
| SUSE Linux Enterprise Server 15 SP2-BCL | Fixed |
| SUSE Linux Enterprise Server 15 SP2-LTSS | Fixed |
| SUSE Linux Enterprise Server 15 SP3 | Fixed |
| SUSE Linux Enterprise Server 15 SP3-BCL | Fixed |
| SUSE Linux Enterprise Server 15 SP3-LTSS | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP1 | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP2 | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP3 | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP4 | Fixed |
| SUSE Linux Enterprise Server for SAP Applications 15 SP5 | Fixed |
| SUSE Manager Proxy 4.0 | Fixed |
| SUSE Manager Proxy 4.1 | Fixed |
| SUSE Manager Proxy 4.2 | Fixed |
| SUSE Manager Retail Branch Server 4.0 | Fixed |
| SUSE Manager Retail Branch Server 4.1 | Fixed |
| SUSE Manager Retail Branch Server 4.2 | Fixed |
| SUSE Manager Server 4.0 | Fixed |
| SUSE Manager Server 4.1 | Fixed |
| SUSE Manager Server 4.2 | Fixed |
| openSUSE Leap 15.3 | Fixed |
| openSUSE Leap 15.4 | Fixed |
| openSUSE Leap 15.5 | Fixed |
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-28288
GHSA-2v35-w6hq-6mfw