GHSA-jxjr-3g7g-3944
xmldom: requireWellFormed element/attribute name validation is bypassable via an embedded line terminator
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| npm | @xmldom/xmldom | >= 0.9.11, < 0.9.12 | 0.9.12 |
Details
As written in the advisory (Markdown source, shown as plain text).
## Summary
An embedded line terminator bypasses the `requireWellFormed` serializer check for element and
attribute names. The check was added to fix GHSA-w2rr-34g9-rvrj and GHSA-4w3w-2rp5-g8jm; a name whose
first line is well-formed slips past it and is serialized verbatim, so the characters after the line
terminator break out of the start/end tag or attribute. Callers who enabled `requireWellFormed`
specifically to neutralize those name-injection issues remain exposed.
## Details
xmldom builds every grammar production through a shared regexp builder that compiles with the `m`
flag. The anchored full-string matcher used for element and attribute names, `QName_exact =
reg('^', QName, '$')`, therefore inherits `m`. When it is applied as `QName_exact.test(name)` against
an already-assembled node name, the `m` flag makes `$` match at an interior line terminator, so the
matcher accepts any value in which **at least one line** is a valid `QName`; the other lines are never
constrained. A payload whose first line is a valid `QName`, followed by a line terminator and breakout
markup, is what yields a working injection.
The serializer emits the accepted name verbatim into element start/end tags and attribute names, so
the bytes after the line terminator break out of the intended syntactic position. The check is
reached whenever a caller serializes, with `requireWellFormed: true`, a node whose name was set
through programmatic DOM construction (`createElement`, `createElementNS`, `createAttribute`,
`createAttributeNS`) with attacker-influenced input.
### Root Cause
1. A shared regexp builder compiles anchored productions with the `m` flag.
2. `^…$` under `m` are line anchors, not string anchors.
3. A full-string validator built on such a production (`.test()`) accepts any string with one
conforming line, so a line terminator followed by breakout markup passes.
The triggering line terminators are the ECMAScript `LineTerminator` set: U+000A, U+000D, U+2028, U+2029.
## Proof of Concept
```js
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
```
## Impact
- **Bypass of a previously shipped security mitigation.** Applications that adopted
`requireWellFormed: true` specifically to neutralize GHSA-w2rr-34g9-rvrj / GHSA-4w3w-2rp5-g8jm
remain exposed to element/attribute name injection.
- **XML / markup structure injection**, and, where the serialized output is placed into an HTML
context, downstream XSS.
## Fix Applied
The anchored XML `Name`/`QName` validators used by the `requireWellFormed` serializer no
longer treat interior line terminators as satisfying the anchors, so a name is validated against the
whole string. A name containing a line terminator is rejected with `InvalidStateError`, closing the
bypass for element and attribute names. The default serialization path is unchanged.
> **⚠ 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
```js
const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');
const doc = new DOMImplementation().createDocument(null, 'root', null);
const el = doc.createElement('a\n><script>alert(1)</script');
doc.documentElement.appendChild(el);
// Default path (require-well-formed off) — unchanged, still emits the name verbatim,
// so the `><script>…` bytes break out of the start tag:
new XMLSerializer().serializeToString(doc);
// Opted-in path — now rejected:
new XMLSerializer().serializeToString(doc, { requireWellFormed: true });
// throws InvalidStateError: The element name "a\n><script>alert(1)</script" is not a valid XML QName
```
### Why the default stays verbatim
The W3C DOM Parsing require-well-formed flag defaults to false, and browser `XMLSerializer` emits
names verbatim when it is unset. Throwing unconditionally would be an unjustified breaking change, so
the check stays gated on the caller opting in with `{ requireWellFormed: true }`.
### Residual limitation
The default serialization path (no `requireWellFormed`) still emits names verbatim by design (above).
Names introduced through `createElement` / `setAttribute` are never validated at creation — those APIs
store the name unchecked by design — so the opt-in serializer check remains the only guard on that
path.
References
External links from the advisory. They lead to third-party sites.
- https://github.com/xmldom/xmldom/security/advisories/GHSA-jxjr-3g7g-3944
- https://nvd.nist.gov/vuln/detail/CVE-2026-83617
- https://github.com/xmldom/xmldom/pull/1071
- https://github.com/xmldom/xmldom/commit/7b2ec67e1750daadd0bb06c92e875e726544a362
- https://github.com/xmldom/xmldom
- https://github.com/xmldom/xmldom/releases/tag/0.9.12