Skip to content

chore(deps): update dependency @xmldom/xmldom to v0.9.12 [security] - #1152

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-xmldom-xmldom-vulnerability
Open

chore(deps): update dependency @xmldom/xmldom to v0.9.12 [security]#1152
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-xmldom-xmldom-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
@xmldom/xmldom 0.9.100.9.12 age confidence

xmldom: XML fragment injection via invalid EntityReference.nodeName during requireWellFormed serialization

CVE-2026-83610 / GHSA-6gmq-8vp8-gcm6

More information

Details

Summary

An EntityReference node can be created with an invalid, attacker-controlled name through Document.createEntityReference(name). When this node is serialized directly with:

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 <injected/>, 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:

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:

safe; <injected/> &x

is serialized as:

&safe; <injected/> &x;

When this fragment is later parsed in an XML context, <injected/> 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:

@xmldom/xmldom@0.9.10
Node.js v24.18.0
Windows 10 / PowerShell
'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(`<root>${fragment}</root>`, 'application/xml');
    return parsed.getElementsByTagName('injected').length;
  } catch (e) {
    return `PARSE_THROW ${e.name}: ${e.message}`;
  }
}

for (const name of [
  'safe',
  'safe; <injected/> &x',
  'x<injected',
  'x y'
]) {
  try {
    const ref = doc.createEntityReference(name);
    const xml = serializer.serializeToString(ref, { requireWellFormed: true });

    console.log(`[SERIALIZED] ${JSON.stringify(name)}: ${xml}`);
    console.log(`[INJECTED_COUNT] ${JSON.stringify(name)}: ${countInjected(xml)}`);
  } catch (e) {
    console.log(`[THROW] ${JSON.stringify(name)}: ${e.name}: ${e.message}`);
  }
}

Observed output:

[SERIALIZED] "safe": &safe;
[INJECTED_COUNT] "safe": 0

[SERIALIZED] "safe; <injected/> &x": &safe; <injected/> &x;
[INJECTED_COUNT] "safe; <injected/> &x": 1

[SERIALIZED] "x<injected": &x<injected;
[INJECTED_COUNT] "x<injected": 0

[SERIALIZED] "x y": &x y;
[INJECTED_COUNT] "x y": 0

Impact

An application that creates an EntityReference from attacker-controlled input and then serializes that node or XML fragment with requireWellFormed: true may produce XML containing attacker-controlled markup.

The impact is limited by two observations:

  1. The parser does not create EntityReference nodes from ordinary XML entity references.
  2. Appending an EntityReference node as an element child is rejected with a HierarchyRequestError.

The main affected scenario is applications that directly use createEntityReference(name) and then serialize the resulting node or fragment.

Fix Applied

Two complementary, non-breaking fixes.
(1) document.createEntityReference(name) rejects an invalid Name at creation, closing the reachable creation vector by default — the opt-in serializer check alone cannot, since a later nodeName mutation would bypass a creation-only guard.
(2) Under requireWellFormed, the serializer validates the EntityReference nodeName as a well-formed XML Name and throws InvalidStateError when it is not; a valid reference still serializes as &name;. Both ship on both maintained versions. The EntityReference / createEntityReference docs note that under requireWellFormed the nodeName is validated as an XML Name, and that xmldom does not expand entities. See the XML Name production.

⚠ 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
'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; <injected/> &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; <injected/> &x';
console.log(serializer.serializeToString(ref));
// -> &safe; <injected/> &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 }.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: 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

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

xmldom/xmldom (@​xmldom/xmldom)

v0.9.12

Compare Source

Fixed
  • Security: parsing a deeply or repeatedly namespaced document no longer consumes quadratic memory; the in-scope namespace map is inherited through the prototype chain instead of being copied for every prefix-declaring element (O(N) instead of O(N²)), preventing a denial-of-service reachable from DOMParser.parseFromString with default options. Serialized output is byte-identical. GHSA-965w-775f-mr7g
  • Security: attribute de-duplication during parsing is now O(M) instead of O(M²); the NamedNodeMap parse-time dedup path uses a null-prototype membership index, so a well-formed document with a hostile number of duplicate attributes can no longer wedge the parse. Attribute order and duplicate resolution (last value wins, first position kept) are byte-identical, preserving the XML no-duplicate-attributes well-formedness constraint. GHSA-8344-3jmq-59r6
  • Security: HTML raw-text parsing no longer amplifies output on a missing or case-mismatched closing tag; the closing tag is matched case-insensitively per the WHATWG HTML RAWTEXT end-tag rule and a missing closing tag is handled explicitly, preventing a denial-of-service. Output for well-formed input is unchanged. GHSA-6mj3-qw4j-hgrw
  • Security: malformed-input recovery is now linear instead of quadratic — the malformed tag-name scan terminates at an embedded <, and Node.prototype.normalize() merges adjacent text nodes in O(K) instead of O(K²) (also reachable programmatically), per normalize() in the WHATWG DOM spec. DOM output is unchanged; only the reported error text differs. GHSA-93r5-fhx6-vmg9
  • Security: XMLSerializer.serializeToString() under { requireWellFormed: true } now rejects a DocType name that is not a valid XML Name, throwing InvalidStateError — matching the sibling publicId/systemId/internalSubset checks and preventing XML injection via DocumentType.name. GHSA-27p8-2357-5qqv
  • Security: XMLSerializer.serializeToString() under { requireWellFormed: true } now validates a processing-instruction target as an XML NCName and rejects a case-insensitive xml, throwing InvalidStateError — preventing PI-target injection via >, ?, or whitespace. GHSA-c7q8-3ch8-vqpv
  • Security: Document.createEntityReference() now rejects an invalid XML Name at creation, and XMLSerializer.serializeToString() under { requireWellFormed: true } validates an EntityReference nodeName as an XML Name, throwing InvalidStateError — preventing XML injection via an entity-reference name. GHSA-6gmq-8vp8-gcm6
  • Security: the requireWellFormed serializer's element- and attribute-name validators no longer treat an interior line terminator as satisfying the name anchors, so a name containing a line terminator is rejected with InvalidStateError — closing a bypass of the XML QName check. GHSA-jxjr-3g7g-3944
  • Security: the requireWellFormed serializer's DocType publicId/systemId validators no longer treat an interior line terminator as satisfying the anchor, so an identifier containing an ECMAScript line terminator is rejected with InvalidStateError — closing a bypass of the XML PubidLiteral/SystemLiteral check. GHSA-vr34-hp96-76pp
  • Security: createElementNS(), createAttributeNS(), createDocumentType(), and createAttribute() now reject a name containing a line terminator with InvalidCharacterError, because name validation applies to the whole string — closing a creation-time bypass of the XML Name/QName production on the default serialization path. GHSA-3px3-54cx-rmw9
  • Security: the parser now reports a not-well-formed end tag whose valid name is followed by trailing content (a recoverable error in XML, a warning in HTML) instead of accepting it silently, per the XML ETag production; parsing recovers to the byte-identical DOM. Consumers that want strict rejection can escalate the reported error to fatal via the parser's onError handler. GHSA-6h8r-xr42-gp59
  • DOMExceptions raised during parsing are now reported as a fatalError, and the originating error is preserved as the cause on the resulting ParseError.
Chore
  • updated dependencies

Thank you,
@​ericchiang,
@​KarimTantawey,
@​bhaswanthc,
@​arpitjain099,
@​Paranoidgrinch,
for your contributions

v0.9.11

Compare Source

Fixed
  • Security: XMLSerializer.serializeToString() now also rejects invalid element and attribute names when { requireWellFormed: true } is passed, throwing InvalidStateError for a name that is not a valid XML QName (this covers the namespace prefix, which surfaces in the element qualified name or in a synthesized xmlns: declaration). This prevents XML injection via createElement() / setAttribute(), extending the existing requireWellFormed checks to the serialized name set. GHSA-w2rr-34g9-rvrj GHSA-4w3w-2rp5-g8jm
  • Security: the processing-instruction grammar regex no longer backtracks quadratically on an unterminated processing instruction (<?… with no closing ?>), preventing a denial-of-service (ReDoS) reachable from DOMParser.parseFromString with default options. GHSA-g53g-w8rj-fmg7
  • CharacterData nodeValue and data are now kept in sync #990
Chore
  • updated dependencies

Thank you,
@​bhaswanthc,
@​jmestwa-coder,
@​stevenobiajulu,
for your contributions


Configuration

📅 Schedule: (in timezone Europe/Berlin)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.83%. Comparing base (983884e) to head (9e03b0a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1152      +/-   ##
==========================================
- Coverage   63.91%   63.83%   -0.09%     
==========================================
  Files          19       19              
  Lines        2425     2425              
  Branches      575      575              
==========================================
- Hits         1550     1548       -2     
- Misses        875      877       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants