Описание
xmldom: Quadratic-memory consumption
Summary
When an element declares a namespace prefix, xmldom copies the entire in-scope namespace map into a fresh object and keeps that copy on the element while it is open on the parse stack. A crafted document that nests N elements, each declaring one unique prefix, therefore drives the parser to hold on the order of N(N+1)/2 = O(N²) namespace-map entries at its peak, so a small, highly compressible input exhausts the heap. Parsing runs under default options on untrusted, network-delivered XML, so a sub-megabyte payload can OOM-crash the process before any application-level validation runs — an unauthenticated denial of service.
Details
appendElement performs the copy: _copy clones the current namespace map into a fresh object for
each prefix-declaring element, and the copy is retained on that element's parse-stack entry:
https://github.com/xmldom/xmldom/blob/08a22d78e4bc50f12ce9f5090b8d96ee6031ac7b/lib/sax.js#L467-L540
The copies stack: the element at depth i copies a map of size ~i, and every ancestor stays live
on the parse stack until it closes, so at the deepest point Σi namespace entries are held at once.
That peak is transient — the completed DOM retains only O(N), one small namespace map per node — but
it is reached during parsing, which is what OOM-crashes the process.
Proof of Concept
A minimal document — N nested elements, each declaring one unique namespace prefix (no SAML wrapper needed):
Measured on Node.js v24 (peak RSS ~quadruples per doubling of depth; absolute numbers vary by host):
| depth | input | peak RSS |
|---|---|---|
| 2,000 | 56 KB | 266 MB |
| 4,000 | 115 KB | 622 MB |
| 8,000 | 232 KB | 1.9 GB |
| 16,000 | ~470 KB | OOM crash (default ~4 GB heap) |
About 470 KB of trivially-generated, highly-compressible input crashes a default Node.js process; larger depths scale as O(N²) into the tens of GB, crashing larger hosts (as first measured by the reporter with a SAML-shaped payload).
Impact
Unauthenticated denial of service against any service that parses attacker-influenced XML with xmldom under default options. A single sub-megabyte request drives multi-gigabyte peak memory and can OOM-crash the process before any application-level validation (e.g. schema checks or a SAML signature verification) runs. The payload is a plain namespace-nesting document and highly compressible, so it is effective over compressed transports (e.g. an HTTP-Redirect / DEFLATE binding, not only POST bindings).
Severity note
The CVSS 4.0 vector scores availability only (VC:N/VI:N/VA:H): the flaw neither discloses nor
alters data, it exhausts the heap. VA:H is justified because a single unauthenticated,
network-delivered request (AV:N/PR:N/UI:N) of trivial complexity (AC:L/AT:N) drives the parser
to multi-gigabyte peak memory and OOM-crashes the process before any application-level logic runs —
a full loss of availability for the affected service.
Fix Applied
Inherit each element's in-scope namespace map through the prototype chain instead of copying it for every prefix-declaring element, so a deeply namespaced document holds O(N) namespace entries instead of O(N²) at peak. Behavior-preserving: serialized output is byte-identical, only the memory cost drops. Non-breaking and independent of requireWellFormed; ships on both maintained versions.
Ссылки
- https://github.com/xmldom/xmldom/security/advisories/GHSA-965w-775f-mr7g
- https://nvd.nist.gov/vuln/detail/CVE-2026-83615
- https://github.com/xmldom/xmldom/pull/1071
- https://github.com/xmldom/xmldom/pull/1072
- https://github.com/xmldom/xmldom/commit/954370f58c046223faf95ba77efcbc8ce014409d
- https://github.com/xmldom/xmldom/commit/dabffe884e864eeecb1f515c716f875e1bc47ec1
- https://github.com/xmldom/xmldom/releases/tag/0.8.15
- https://github.com/xmldom/xmldom/releases/tag/0.9.12
Пакеты
@xmldom/xmldom
>= 0.7.0, <= 0.8.14
0.8.15
@xmldom/xmldom
>= 0.9.0, <= 0.9.11
0.9.12
xmldom
>= 0.1.5, <= 0.6.0
Отсутствует
Связанные уязвимости
(xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) ...)
xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. Prior to @xmldom/xmldom versions 0.8.15 and 0.9.12, and in xmldom versions 0.1.5 through 0.6.0, appendElement in lib/sax.js uses _copy to clone the complete currentNSMap for each nested element that declares a new namespace prefix. Keeping every ancestor map live on the parse stack creates quadratic peak namespace-map storage, so a small highly compressible XML document can exhaust the process heap before application validation. This issue is fixed in @xmldom/xmldom versions 0.8.15 and 0.9.12; no fixed version is available for xmldom.
xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. Prior to @xmldom/xmldom versions 0.8.15 and 0.9.12, and in xmldom versions 0.1.5 through 0.6.0, appendElement in lib/sax.js uses _copy to clone the complete currentNSMap for each nested element that declares a new namespace prefix. Keeping every ancestor map live on the parse stack creates quadratic peak namespace-map storage, so a small highly compressible XML document can exhaust the process heap before application validation. This issue is fixed in @xmldom/xmldom versions 0.8.15 and 0.9.12; no fixed version is available for xmldom.
xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) ...