highCVE-2026-83618Sep 8, 2026

CVE-2026-83618: @xmldom/xmldom requireWellFormed DocType Injection Bypass via Embedded Line Terminator

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Package@xmldom/xmldom
Ecosystemnpm
Affected>= 0.9.10, <= 0.9.11
Fixed in0.9.12
CVE-2026-83618: @xmldom/xmldom requireWellFormed DocType Injection Bypass via Embedded Line Terminator

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.

javascript
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.

Reporter not attributed.

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

Related research