GHSA-jxjr-3g7g-3944: XSS

Published Sep 8, 2026
·
Updated

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

1 affected componentFixes available
npm/@xmldom/xmldom=0.9.11
0.9.12

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@xmldom/xmldom to a version that resolves this vulnerability.

    Fixed in 0.9.12
  2. 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
  3. 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

Sep 8, 2026
Advisory Published
via GitHub·09:03 PM
Data Sourced
via GitHub·09:03 PM
DescriptionWeaknessAffected Software

Frequently Asked Questions

1

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.

2

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.

3

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203