CVE-2026-41673

CVE-2026-41673 is a high-severity security vulnerability in @xmldom/xmldom (npm), affecting versions < 0.8.13. It is fixed in 0.8.13, 0.9.10.

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

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 exceeded

XMLSerializer.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 exceeded

The other methods have been verified using similar pocs.

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.

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.

Affected versions

@xmldom/xmldom (< 0.8.13) @xmldom/xmldom (>= 0.9.0, < 0.9.10) xmldom (<= 0.6.0)

Security releases

@xmldom/xmldom → 0.8.13 (npm) @xmldom/xmldom → 0.9.10 (npm)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

See it in your environment

Remediation advice

Upgrade the following packages to resolve this vulnerability:

@xmldom/xmldom to 0.8.13 or later; @xmldom/xmldom to 0.9.10 or later

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-41673? CVE-2026-41673 is a high-severity security vulnerability in @xmldom/xmldom (npm), affecting versions < 0.8.13. It is fixed in 0.8.13, 0.9.10.
  2. Which packages are affected by CVE-2026-41673?
    • @xmldom/xmldom (npm) (versions < 0.8.13)
    • xmldom (npm) (versions <= 0.6.0)
  3. Is there a fix for CVE-2026-41673? Yes. CVE-2026-41673 is fixed in 0.8.13, 0.9.10. Upgrade to this version or later.
  4. Is CVE-2026-41673 exploitable, and should I be worried? Whether CVE-2026-41673 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  5. What actually determines whether CVE-2026-41673 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  6. How do I fix CVE-2026-41673?
    • Upgrade @xmldom/xmldom to 0.8.13 or later
    • Upgrade @xmldom/xmldom to 0.9.10 or later

Other vulnerabilities in @xmldom/xmldom

CVE-2026-41674CVE-2026-41675CVE-2026-41672CVE-2026-34601CVE-2022-39353

Stop the waste.
Protect your environment with Kodem.