GitHub Advisoryadvisory · npm

GHSA-6gmq-8vp8-gcm6 xmldom: XML fragment injection via invalid EntityReference.nodeName during requireWellFormed serialization

MEDIUM Published 2026-09-02 Modified 2026-09-02 Official page ↗
Aliases
CVE-2026-83610
Ecosystem
npm
Packages
@xmldom/xmldom, xmldom
Severity
Moderate
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-116
Reviewed
2026-09-02

Affected packages

• npm @xmldom/xmldom: >= 0.7.0, < 0.8.15 → fixed in 0.8.15 • npm @xmldom/xmldom: >= 0.9.0, < 0.9.12 → fixed in 0.9.12 • npm xmldom: <= 0.6.0 (no fix yet)

Description

Summary An `EntityReference` node can be created with an invalid, attacker-controlled name through `Document.createEntityReference(name)`. When this node is serialized directly with: ```js serializer.serializeToString(ref, { requireWellFormed: true }) ``` the invalid `nodeName` is emitted into the serialized XML fragment without validation or escaping. This can produce real XML markup in the serialized output. In the proof of concept below, the serialized fragment contains ``, and reparsing the fragment creates a real `injected` element. --- Details The issue appears to be in the serialization path for `ENTITY_REFERENCE_NODE`. For several other node types, `requireWellFormed: true` performs specific validation checks before serialization. For example, comments, processing instructions, document types, and some character data cases are checked before being emitted. However, for `ENTITY_REFERENCE_NODE`, the serializer appears to emit the node name directly in entity reference form: ```js case ENTITY_REFERENCE_NODE: buf.push('&', n.nodeName, ';'); return null; ``` As a result, if `nodeName` contains characters that break out of the intended `&name;` structure, the serializer can emit additional XML markup. For example, an entity reference created with the name: ```text safe; &x ``` is serialized as: ```xml &safe; &x; ``` When this fragment is later parsed in an XML context, `` becomes a real element. This is especially surprising when `{ requireWellFormed: true }` is used, because applications may reasonably treat this mode as the stricter or safer XML serialization mode. --- Proof of Concept Tested with: ```text @xmldom/[email protected] Node.js v24.18.0 Windows 10 / PowerShell ``` ```js 'use strict'; const { DOMImplementation, XMLSerializer, DOMParser } = require('@xmldom/xmldom'); const impl = new DOMImplementation(); const doc = impl.createDocument(null, 'root', null); const serializer = new XMLSerializer(); function countInjected(fragment) { try { const parsed = new DOMParser().parseFromString(`${fragment}`, 'application/xml'); return parsed.getElementsByTagName('injected').length; } catch (e) { return `PARSE_THROW ${e.name}: ${e.message}`; } } for (const name of [ 'safe', 'safe; &x', 'x &x": &safe; &x; [INJECTED_COUNT] "safe; &x": 1 [SERIALIZED] "x ⚠ Opt-in required. Protection is not automatic. Existing serialization calls remain > vulnerable unless `{ requireWellFormed: true }` is explicitly passed. Applications that > serialize untrusted DOM content should audit all `serializeToString()` call sites and add it. Proof of Concept - fixed path ```js 'use strict'; const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom'); const impl = new DOMImplementation(); const doc = impl.createDocument(null, 'root', null); const serializer = new XMLSerializer(); // Creation-time anchor (applied by default): an invalid XML Name is rejected at creation. try { doc.createEntityReference('safe; &x'); } catch (e) { console.log(`${e.name}`); // rejected at creation } // Default path (requireWellFormed omitted): because creation now rejects an ill-formed name, // an ill-formed nodeName is only reachable via a post-creation mutation — and is emitted verbatim. const ref = doc.createEntityReference('safe'); ref.nodeName = 'safe; &x'; console.log(serializer.serializeToString(ref)); // -> &safe; &x; (injection present on the default path) // Opt-in path: throws on the invalid nodeName. try { serializer.serializeToString(ref, { requireWellFormed: true }); } catch (e) { console.log(`${e.name}`); // InvalidStateError } // A valid name still serializes as &name; under requireWellFormed. const ok = doc.createEntityReference('valid'); console.log(serializer.serializeToString(ok, { requireWellFormed: true })); // -> &valid; ``` Why the default stays verbatim The creation-time anchor is applied by default, because it is classified non-breaking. The serializer check, by contrast, stays gated behind `{ requireWellFormed: true }`: W3C DOM Parsing's require-well-formed flag defaults to `false`, and the browser `XMLSerializer` emits the `nodeName` verbatim in that default mode, so unconditionally throwing for an ill-formed `EntityReference.nodeName` would be an unjustified breaking change — which is why the default serialization path stays verbatim. Residual limitation The creation vector is closed by default — the non-breaking creation-time anchor — with no further deferred work. The residual is at serialization: the default path still emits an ill-formed `nodeName` verbatim, because the serializer check is opt-in via `{ requireWellFormed: true }`.

Related

Alias · CVE (1)

  • CVE-2026-83610 xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. vulnerability

Weakness · CWE (1)

  • CWE-116 Improper Encoding or Escaping of Output weakness

References