GitHub Advisoryadvisory · npm
GHSA-jxjr-3g7g-3944 xmldom: requireWellFormed element/attribute name validation is bypassable via an embedded line terminator
HIGH
Official page ↗
- Aliases
- CVE-2026-83617
- Ecosystem
- npm
- Packages
- @xmldom/xmldom
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
- Weaknesses
- CWE-625, CWE-91
- Reviewed
- 2026-09-08
Affected packages
• npm @xmldom/xmldom: >= 0.9.11, < 0.9.12 → fixed in 0.9.12
Description
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>alert(1)…`
// 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>alert(1)…` 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>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.
Related
Alias · CVE (1)
- CVE-2026-83617 xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. vulnerability
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