GHSA-27p8-2357-5qqv
xmldom: DocType `name` Injection Bypasses requireWellFormed
Quick fix
GHSA-27p8-2357-5qqv — @xmldom/xmldom: upgrade to the fixed version with the command below.
npm install @xmldom/xmldom@0.8.15Details
## 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`](https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L3256-L3283) — the `requireWellFormed` block ([#L3259-L3269](https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L3259-L3269)) validates `publicId`/`systemId`/`internalSubset` but not `name`, which is emitted verbatim at [#L3270](https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L3270). - 0.8.x (v0.8.13, `e5c1480`): [serializer DocType case, `lib/dom.js#L1914-L1946`](https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom.js#L1914-L1946) — same structure; `name` is emitted verbatim at [#L1928](https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/dom.js#L1928). - unscoped `xmldom` (v0.6.0, `c80a161`): [`lib/dom.js#L1105`](https://github.com/xmldom/xmldom/blob/c80a161172cc4d8733583bf0cf59abfa589f6d9e/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`](https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#L925-L936), validation at [#L926](https://github.com/xmldom/xmldom/blob/bb7a085dc5ba1eea3212388509b97bb4b4af32b9/lib/dom.js#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`](https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/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`](https://github.com/xmldom/xmldom/blob/c80a161172cc4d8733583bf0cf59abfa589f6d9e/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](https://www.w3.org/TR/xml/#NT-Name). > **⚠ 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.
Are you affected?
Enter the version of the package you're using.
Affected packages
0No fixed version published yet for xmldom (npm). Pin to a known-safe version or switch to an alternative.
References
- https://github.com/xmldom/xmldom/security/advisories/GHSA-27p8-2357-5qqv[WEB]
- https://nvd.nist.gov/vuln/detail/CVE-2026-83608[ADVISORY]
- https://github.com/xmldom/xmldom/pull/1071[WEB]
- https://github.com/xmldom/xmldom/pull/1072[WEB]
- https://github.com/xmldom/xmldom/commit/57aec90ac57b4408ae7c5d1746bf2a693b5ed90e[WEB]
- https://github.com/xmldom/xmldom/commit/85f12eb4d14b44de33216cfb72b50af4d24e9fdd[WEB]
- https://github.com/xmldom/xmldom[PACKAGE]
- https://github.com/xmldom/xmldom/releases/tag/0.8.15[WEB]
- https://github.com/xmldom/xmldom/releases/tag/0.9.12[WEB]