GHSA-27p8-2357-5qqv CVE-2026-83608
xmldom: DocType `name` Injection Bypasses requireWellFormed
- Severity
- 0.0 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.61%
- Chance of exploitation in the next 30 days. Higher than 49.31% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-91 XML Injection (aka Blind XPath Injection)
- Fix
0.8.15- Upgrade @xmldom/xmldom to this version or later
Description
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 DOCUMENT_TYPE_NODE 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— therequireWellFormedblock (#L3259-L3269) validatespublicId/systemId/internalSubsetbut notname, which is emitted verbatim at #L3270. - 0.8.x (v0.8.13,
e5c1480): serializer DocType case,lib/dom.js#L1914-L1946— same structure;nameis emitted verbatim at #L1928. - unscoped
xmldom(v0.6.0,c80a161):lib/dom.js#L1105emitsnode.nameverbatim; this line predatesrequireWellFormed, 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 viavalidateQualifiedName(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 throughcreateDocumentType()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
- The serializer's
requireWellFormedDocType block checkspublicId,systemId, andinternalSubset(the fields hardened by GHSA-f6ww-3ggp-fr8h) but has no check forname. DocumentType.nameis a plain writable own-property; on 0.8.x and the unscoped packagecreateDocumentType()does not validate it either.- The serializer emits
namedirectly between the doctype delimiters:<!DOCTYPE ${name}…>.
Proof of Concept
Run against @xmldom/xmldom v0.9.10 (commit bb7a085):
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 todt.namebypasses that, andserializeToString(…, { 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 withHierarchyRequestError: 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), andserializeToString(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: trueas a mitigation for the prior injection CVEs (including the sibling DocType fields fixed by GHSA-f6ww-3ggp-fr8h) remain vulnerable through the DocTypename.
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 allserializeToString()call sites and add it.
Proof of Concept - fixed path
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.
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| @xmldom/xmldom | >= 0.7.0, < 0.8.15 |
0.8.15 |
| @xmldom/xmldom | >= 0.9.0, < 0.9.12 |
0.9.12 |
| xmldom | <= 0.6.0 |
No fix yet |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
@xmldom/xmldom npm → 0.8.15
- npm
npm install @xmldom/xmldom@0.8.15- yarn
yarn add @xmldom/xmldom@0.8.15- pnpm
pnpm add @xmldom/xmldom@0.8.15
Severity breakdown
CVSS 4.0 0.0
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- None
- User interaction
- None
- Confidentiality (vulnerable system)
- None
- Integrity (vulnerable system)
- High
- Availability (vulnerable system)
- None
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
Weakness
CWE-91: XML Injection (aka Blind XPath Injection)
The product does not properly neutralize special elements that are used in XML, allowing attackers to modify the syntax, content, or commands of the XML before it is processed by an end system.
How to prevent it
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
References
Advisories
Packages
Other links
- github.com/xmldom/xmldom/security/advisories/GHSA-27p8-2357-5qqv
- github.com/xmldom/xmldom/pull/1071
- github.com/xmldom/xmldom/pull/1072
- github.com/xmldom/xmldom/commit/57aec90ac57b4408ae7c5d1746bf2a693b5ed90e
- github.com/xmldom/xmldom/commit/85f12eb4d14b44de33216cfb72b50af4d24e9fdd
- github.com/xmldom/xmldom/releases/tag/0.8.15
- github.com/xmldom/xmldom/releases/tag/0.9.12
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-8344-3jmq-59r6 xmldom: Quadratic-time attribute deduplication | High | Linked by the source |
| GHSA-x4fp-j954-r2f4 xmldom: End-tag Whitespace-Trim Regex ReDoS — quadratic backtracking in the 0.8.x end-tag parser | High | Also affects @xmldom/xmldom |
| GHSA-vr34-hp96-76pp xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator | High | Also affects @xmldom/xmldom |
| GHSA-jxjr-3g7g-3944 xmldom: requireWellFormed element/attribute name validation is bypassable via an embedded line terminator | High | Also affects @xmldom/xmldom |
| GHSA-c7q8-3ch8-vqpv xmldom: Processing Instruction Target Injection Bypasses requireWellFormed | High | Also affects xmldom |
| GHSA-965w-775f-mr7g xmldom: Quadratic-memory consumption | High | Also affects xmldom |
| GHSA-93r5-fhx6-vmg9 xmldom: Quadratic-time parsing via the malformed-input recovery path — `parseElementStartPart` re-scan and `normalize()` adjacent-text merge | High | Also affects xmldom |
| GHSA-6mj3-qw4j-hgrw xmldom: HTML raw-text closing-tag case mismatch causes output amplification | High | Also affects @xmldom/xmldom |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor