GHSA-jxjr-3g7g-3944: XSS
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, QNameexact = reg('^', QName, '$'), therefore inherits m. When it is applied as QNameexact.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.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/@xmldom/xmldomto a version that resolves this vulnerability.Fixed in 0.9.12 - Configuration
For any serializeToString() of untrusted DOM/markup, call new XMLSerializer().serializeToString(doc, { requireWellFormed: true }) to ensure element/attribute names are validated (required to neutralize GHSA-w2rr-34g9-rvrj / GHSA-4w3w-2rp5-g8jm).
XMLSerializer (xmldom) requireWellFormed = true - Operational
Audit all serializeToString() call sites that may serialize attacker-influenced DOM content (e.g., nodes created via createElement/createAttributeNS) and ensure they pass { requireWellFormed: true }; existing call sites that do not opt in remain vulnerable to name injection via embedded LineTerminator (U+000A, U+000D, U+2028, U+2029).
Event History
Frequently Asked Questions
What attacker-controlled input is needed to exploit this issue?
An attacker needs control over an element or attribute name that the application later serializes. The name must begin with a valid QName, then include a line terminator followed by markup intended to break out of the element tag, end tag, or attribute.
Does enabling requireWellFormed prevent this injection?
No. The affected name validation can accept a multi-line value when at least one line, including the first line used by the payload, is a valid QName; the serializer then emits the full name verbatim.
Which applications are realistically exposed?
Applications using npm/@xmldom/xmldom are exposed when untrusted data can influence serialized element or attribute names. The issue is especially relevant to callers that relied on requireWellFormed to mitigate prior name-injection issues.