Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 npm
Not in CISA KEV

CVE-2026-41673 @xmldom/xmldom

Fix: xmldom/xmldom@17678a2

CVE-2026-41673 is a CWE-674 vulnerability in @xmldom/xmldom. A fix is available for @xmldom/xmldom — see the affected versions and patch details below.

xmldom: Denial of service via uncontrolled recursion in XML serialization

Also known asGHSA-2v35-w6hq-6mfw
Published
May 7, 2026
Updated
Aug 30, 2026
Affected
3 pkgs
Patched
2 / 3
Exploits
None indexed
Exploitation data as of Sep 22, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-41673.

EPSS Exploitation Probability

via FIRST.org ↗
0.6%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs49th percentile — riskier than 49% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

Real-World Exposure

3 pkgs affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

1Kother npm packages depend on this — each one inherits the vulnerability until it's patched upstream
@xmldom/xmldomnpm
36.9Mdownloads / week
xmldomnpm
1.3Mdownloads / week

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

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.

FunctionDefinitionPublic API entry point(s)Crash depth (Node.js 18)
_visitNodelib/dom.js:1529getElementsByTagName(), getElementsByTagNameNS(), getElementsByClassName(), getElementById()~10,000 levels
cloneNode (module fn)lib/dom.js:3037Node.prototype.cloneNode(true)~10,000 levels
importNode (module fn)lib/dom.js:2975Document.prototype.importNode(node, true)~10,000 levels
getTextContent (inner fn)lib/dom.js:3130node.textContent (getter)~10,000 levels
isEqualNodelib/dom.js:1120Node.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.


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:

OperationPublic entry point(s)
_visitNode helpergetElementsByTagName(), getElementsByTagNameNS(), getElementsByClassName(), getElementById()
getTextContent inner functionnode.textContent getter
cloneNode module functionNode.prototype.cloneNode(true)
importNode module functionDocument.prototype.importNode(node, true)
serializeToString workerXMLSerializer.prototype.serializeToString(), Node.prototype.toString(), NodeList.prototype.toString()
normalizeNode.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.

Affected Packages

3 total 2 fixed
EcosystemPackageVulnerable rangeFix
📦npm@xmldom/xmldomall versions0.8.13npm install @xmldom/xmldom@0.8.13
📦npm@xmldom/xmldom0.9.0&&< 0.9.100.9.10npm install @xmldom/xmldom@0.9.10
📦npmxmldomall versionsNo fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @xmldom/xmldom, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update @xmldom/xmldom to 0.8.13 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-41673 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-41673 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2026-41673. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatImportant

This is an Important denial of service vulnerability in the `xmldom` library, which can lead to application crashes. The flaw occurs when processing specially crafted, deeply nested XML documents, causing excessive recursion and exhausting system resources. This can impact the availability of Red Hat products that…

ProductFixed inAdvisory
Red Hat Developer Hub 1.9rhdh/rhdh-hub-rhel9:1781187342RHSA-2026:26234
Red Hat OpenShift AI 3.4rhoai/odh-workbench-codeserver-datascience-cpu-py312-rhel9:1787121387RHSA-2026:60520

Frequently Asked Questions

## 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()` / `getElement
O3 Security · Impact-Aware SCA

Is CVE-2026-41673 in your dependencies?

O3 Security finds CVE-2026-41673 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2026-41673: @xmldom/xmldom DoS | O3 Security