CVE-2026-83608: xmldom: DocType `name` Injection Bypasses requireWellFormed
Summary
The @xmldom/xmldom serializer emits DocumentType.name verbatim into the <!DOCTYPE …> declaration with no well-formedness guard. GHSA-f6ww-3ggp-fr8h (CVE-2026-41674) hardened the serializer's requireWellFormed path for a DocumentType's sibling fields — publicId, systemId, and internalSubset — but it did not add any check for name. A > (or whitespace) in the name terminates the doctype declaration early, letting the remaining characters become sibling markup in the serialized output.
Because requireWellFormed: true — the recommended mitigation for the prior xmldom injection CVEs — performs no validation on the DocType name, this is a bypass of that control, in the same family as the open element-name (GHSA-w2rr-34g9-rvrj) and attribute-name (GHSA-4w3w-2rp5-g8jm) name-injection advisories.
Details
The serializer's DOCUMENTTYPENODE case runs the requireWellFormed block only against publicId, systemId, and internalSubset, then pushes n.name directly into the buffer between the <!DOCTYPE prefix and the closing >:
- 0.9.x (v0.9.10, bb7a085): serializer DocType case, lib/dom.js#L3256-L3283 — the requireWellFormed block (#L3259-L3269) validates publicId/systemId/internalSubset but not name, which is emitted verbatim at #L3270. - 0.8.x (v0.8.13, e5c1480): serializer DocType case, lib/dom.js#L1914-L1946 — same structure; name is emitted verbatim at #L1928. - unscoped xmldom (v0.6.0, c80a161): lib/dom.js#L1105 emits node.name verbatim; this line predates requireWellFormed, so there is no well-formedness path at all.
Enabling write paths
DocumentType.name is a plain, writable own-property, so the enabling vector differs by line:
- 0.9.x — createDocumentType() validates the name via validateQualifiedName (lib/dom.js#L925-L936, validation at #L926), so the deliverable vector is a direct property write (dt.name = 'html><script>…') to the unguarded own-property. - 0.8.x — createDocumentType() does not validate the name (lib/dom.js#L456-L464), so the malicious name is reachable directly through createDocumentType() as well as via direct property write. - unscoped xmldom (<= 0.6.0) — createDocumentType() does not validate the name (lib/dom.js#L286), same as 0.8.x.
This is the same structural root cause the sibling name-injection advisories share: the serializer's requireWellFormed path validates content delimiters but no name field, and every name-like field is a plain writable property, so mutation / direct property-write bypasses any creation-time check.
Root Cause
1. The serializer's requireWellFormed DocType block checks publicId, systemId, and internalSubset (the fields hardened by GHSA-f6ww-3ggp-fr8h) but has no check for name. 2. DocumentType.name is a plain writable own-property; on 0.8.x and the unscoped package createDocumentType() does not validate it either. 3. The serializer emits name directly between the doctype delimiters: <!DOCTYPE ${name}…>.
Proof of Concept
Run against @xmldom/xmldom v0.9.10 (commit bb7a085):
javascript const { DOMImplementation, XMLSerializer, DOMParser } = require('@xmldom/xmldom');
const impl = new DOMImplementation(); const serializer = new XMLSerializer();
// 0.9.x createDocumentType validates the name, so overwrite it via direct property write const dt = impl.createDocumentType('html', '', ''); dt.name = 'html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script'; const doc = impl.createDocument(null, 'r', dt);
const output = serializer.serializeToString(doc, { requireWellFormed: true }); console.log(output); // Output: <!DOCTYPE html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><r/> // // requireWellFormed: true did NOT prevent the injection (no exception thrown). // The injected <script> is well-formed XHTML that a browser would execute.
Confirmed runtime behavior:
- 0.9.x — createDocumentType() rejects the malicious name at creation (InvalidCharacterError); a direct write to dt.name bypasses that, and serializeToString(…, { requireWellFormed: true }) emits the breakout with no exception. - Re-parse confirmation — re-parsing the output shows the injected <script> is a real second top-level element originating entirely from the DocType name: the parser rejects it with HierarchyRequestError: Only one element can be added and only after doctype. A comment-injection variant (dt.name = 'html><!--INJECTED--', output <!DOCTYPE html><!--INJECTED--><r/>) re-parses cleanly and the injected comment node is enumerable, confirming the injected node is structurally live. - 0.8.x (v0.8.13, e5c1480) — createDocumentType('html><script>…', '', '') accepts the malicious name directly (no creation-time validation), and serializeToString(doc, null, null, { requireWellFormed: true }) produces <!DOCTYPE html><script>alert(1)</script><r/> with no exception.
A browser reproduction does not apply: browsers keep DocumentType.name readonly, so the direct-write vector cannot be reproduced in a browser DOM. The injection is specific to xmldom exposing name as writable and serializing it without a guard.
Impact
Applications that build a DocumentType node with an attacker-influenced name — via direct property write on any affected line, or via createDocumentType() on 0.8.x and the unscoped package — and serialize the document are vulnerable to XML/markup injection:
- XML structure injection — breaking out of the <!DOCTYPE …> declaration to inject arbitrary sibling elements, comments, or additional markup into the output. - XSS via XHTML — if the serialized output is served as XHTML or processed by a browser-based XML parser, an injected <script> element (in the XHTML namespace) executes. - requireWellFormed bypass — applications that adopted requireWellFormed: true as a mitigation for the prior injection CVEs (including the sibling DocType fields fixed by GHSA-f6ww-3ggp-fr8h) remain vulnerable through the DocType name.
Fix Applied
Under requireWellFormed, the serializer validates the DocType name as a well-formed XML Name and throws InvalidStateError when it is not — matching the sibling publicId/systemId/internalSubset checks. Non-breaking and opt-in; ships on both maintained versions. No creation-time change is made: 0.9.x already validates the name at createDocumentType, and the 0.8.x/unscoped creation gap cannot be closed without a breaking change, so it is left unfixed. 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
javascript const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');
const impl = new DOMImplementation(); const serializer = new XMLSerializer();
const dt = impl.createDocumentType('html', '', ''); dt.name = 'html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script'; const doc = impl.createDocument(null, 'r', dt);
// Default path: the ill-formed name is still emitted verbatim (injection present). console.log(serializer.serializeToString(doc)); // <!DOCTYPE html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><r/>
// Opt-in path: serialization throws instead of emitting the breakout. serializer.serializeToString(doc, { requireWellFormed: true }); // throws InvalidStateError
Why the default stays verbatim
W3C DOM Parsing's require-well-formed flag defaults to false, and the browser XMLSerializer emits the name verbatim in that default mode. Unconditionally throwing would be an unjustified breaking change against that specified default, so the guard is opt-in behind { requireWellFormed: true }.
Residual limitation
The default serialization path still emits the ill-formed DocType name verbatim; protection applies only when requireWellFormed: true is passed. No creation-time validation is added for the DocType name: 0.9.x already validates at createDocumentType, and the 0.8.x/unscoped creation gap is left unfixed — it cannot be closed without a breaking change.
Other sources
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 version 0.6.0 and earlier, the DOCUMENTTYPENODE branch in lib/dom.js validates publicId, systemId, and internalSubset under requireWellFormed: true but emits DocumentType.name verbatim. A name containing > or whitespace can terminate the <!DOCTYPE ...> declaration and inject sibling markup; the value can be supplied through createDocumentType() on the 0.8.x and unscoped lines or through a direct DocumentType.name property write on every affected line. The default path and legacy creation-time behavior remain permissive, while the vulnerable strict path fails to enforce an XML Name. This issue is fixed in @xmldom/xmldom versions 0.8.15 and 0.9.12; no fixed version is available for xmldom.
— MITRE
xmldom: DocType name Injection Bypasses requireWellFormed
— Microsoft
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/@xmldom/xmldomto a version that resolves this vulnerability.Fixed in 0.9.12 - Upgrade
Upgrade
npm/@xmldom/xmldomto a version that resolves this vulnerability.Fixed in 0.8.15 - Upgrade
Upgrade
@xmldom/xmldomto a version that resolves this vulnerability.Fixed in 0.8.15 - Upgrade
Upgrade
@xmldom/xmldomto a version that resolves this vulnerability.Fixed in 0.9.12 - Configuration
When serializing DOM that may include attacker-controlled DocumentType fields, call serializeToString with { requireWellFormed: true } (e.g., serializeToString(doc, null, null, { requireWellFormed: true })).
@xmldom/xmldom XMLSerializer / serializeToString requireWellFormed = true - Compensating control
If you must serialize untrusted DOM content, audit all serializeToString() call sites and ensure { requireWellFormed: true } is explicitly passed; the protection is opt-in and not automatic.
Event History
Frequently Asked Questions
Which releases contain a fix?
The issue is fixed in @xmldom/xmldom 0.8.15 and 0.9.12. No fixed version is available for the unscoped xmldom package.
Does enabling requireWellFormed prevent this injection?
No. In affected versions, requireWellFormed validates the public ID, system ID, and internal subset, but does not enforce that DocumentType.name is a valid XML Name before emitting it.
What level of control is needed to exploit the issue?
An attacker needs control of a DocumentType name that is later serialized. On affected 0.8.x and unscoped lines this can be supplied through createDocumentType(), while every affected line permits it through a direct write to DocumentType.name.
Is the default behavior affected?
Yes. The default serialization path remains permissive, and legacy creation-time behavior is also permissive.