GHSA-jxjr-3g7g-3944
xmldom: requireWellFormed element/attribute name validation is bypassable via an embedded line terminator
Quick fix
GHSA-jxjr-3g7g-3944 — @xmldom/xmldom: upgrade to the fixed version with the command below.
npm install @xmldom/xmldom@0.9.12Details
## 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.
Are you affected?
Enter the version of the package you're using.
Affected packages
References
- https://github.com/xmldom/xmldom/security/advisories/GHSA-jxjr-3g7g-3944[WEB]
- https://nvd.nist.gov/vuln/detail/CVE-2026-83617[ADVISORY]
- https://github.com/xmldom/xmldom/pull/1071[WEB]
- https://github.com/xmldom/xmldom/commit/7b2ec67e1750daadd0bb06c92e875e726544a362[WEB]
- https://github.com/xmldom/xmldom[PACKAGE]
- https://github.com/xmldom/xmldom/releases/tag/0.9.12[WEB]