CVE-2026-83618: @xmldom/xmldom requireWellFormed DocType Injection Bypass via Embedded Line Terminator
A newline character inside a publicId or systemId silently passes xmldom's well-formed serializer check and lets attackers inject arbitrary markup into the DOCTYPE declaration, defeating the…

The problem
In @xmldom/xmldom 0.9.10 and 0.9.11, the serializer validates DocumentType publicId and systemId using PubidLiteral_match and SystemLiteral_match, both built by a shared reg() helper in lib/grammar.js that compiles every regex with the multiline (m) flag.
With the m flag active, the $ anchor matches at any interior line terminator, not only at the true end of the string. A publicId or systemId whose first line is a valid PubidLiteral passes the .test() check even when a newline and breakout markup follow on subsequent lines.
The full, unsanitized value, including the injected content, is then written verbatim into the <!DOCTYPE ...> declaration. This directly bypasses the mitigation introduced for the earlier DocumentType injection advisory (GHSA-f6ww-3ggp-fr8h).
Proof of concept
A working proof-of-concept for CVE-2026-83618 in @xmldom/xmldom, with the exact payload below.
const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');
const impl = new DOMImplementation();
// publicId: valid PubidLiteral on line 1, then newline + breakout markup
const dt = impl.createDocumentType(
'html',
'"valid pubid"\n"><!ENTITY xxe SYSTEM "file:///etc/passwd">',
''
);
const doc = impl.createDocument(null, 'root', dt);
// Should throw InvalidStateError — instead silently emits injection:
console.log(new XMLSerializer().serializeToString(doc, { requireWellFormed: true }));
// Output (no throw):
// <!DOCTYPE html PUBLIC "valid pubid"
// "><!ENTITY xxe SYSTEM "file:///etc/passwd">><root/>The root cause is CWE-185 (Incorrect Regular Expression) combined with CWE-91 (XML Injection). The reg() builder in lib/grammar.js always includes the m flag, turning the anchored ^...$ pattern into a line anchor instead of a string anchor. So PubidLiteral_match.test(value) returns true as soon as any single line in value forms a valid PubidLiteral, ignoring everything after the first newline.
The fix (commit 7b2ec67, released in 0.9.12) changes the validators so that an interior ECMAScript LineTerminator (U+000A, U+000D, U+2028, U+2029) no longer satisfies the $ anchor. Any publicId or systemId containing one of those characters now throws InvalidStateError before serialization.
Valid single-line identifiers are unaffected. The default (requireWellFormed: false) path is intentionally left unchanged to match browser and W3C spec behavior.
The fix
Upgrade @xmldom/xmldom to 0.9.12 or later (commit 7b2ec67e1750daadd0bb06c92e875e726544a362, PR #1071). After upgrading, audit every serializeToString() call site and pass { requireWellFormed: true } when serializing untrusted DOM content. Without that option the default path still emits publicId and systemId verbatim by design.
Related research
- highCVE-2026-83609CVE-2026-83609: @xmldom/xmldom XML Injection via Line Terminator in QName Validation
- highCVE-2026-83617CVE-2026-83617: @xmldom/xmldom requireWellFormed QName Check Bypass via Embedded Line Terminator
- highCVE-2026-83607CVE-2026-83607: @xmldom/xmldom Element Name Injection via createElement()
- highCVE-2026-83605CVE-2026-83605: @xmldom/xmldom Attribute Name Injection via setAttribute()