GitHub Advisoryadvisory · npm

GHSA-vr34-hp96-76pp xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator

HIGH Published 2026-09-08 Modified 2026-09-08 Official page ↗
Aliases
CVE-2026-83618
Ecosystem
npm
Packages
@xmldom/xmldom
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-625, CWE-91
Reviewed
2026-09-08

Affected packages

• npm @xmldom/xmldom: >= 0.9.10, < 0.9.12 → fixed in 0.9.12

Description

Summary An embedded line terminator bypasses the `requireWellFormed` serializer check for a `DocumentType`'s publicId and systemId. The check was added to fix GHSA-f6ww-3ggp-fr8h; an id whose first line is a valid literal slips past it and is emitted verbatim into the `` declaration, so the markup after the line terminator breaks out into the surrounding document. Callers who enabled `requireWellFormed` to neutralize DocumentType injection remain exposed. Details `publicId` and `systemId` are stored as raw values including their surrounding quotes, and the `PubidLiteral`/`SystemLiteral` productions include those quotes. The serializer validates them with `g.PubidLiteral_match.test(publicId)` and `g.SystemLiteral_match.test(systemId)`, where both matchers are `reg('^', …, '$')` and inherit the `m` flag from xmldom's shared regexp builder. Under `m`, `$` matches at an interior line terminator, so a value such as `"valid pubid"\n">` satisfies the matcher on its first line (`"valid pubid"` is a complete `PubidLiteral`) and the whole value — including the post-newline breakout — is emitted after `PUBLIC`/`SYSTEM`. 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 complete, valid literal on the first line passes even though a line terminator and breakout markup follow. `PubidChar` excluding `` does not prevent it — the breakout is appended *after* the literal, not embedded inside it. The triggering line terminators are the ECMAScript `LineTerminator` set: U+000A, U+000D, U+2028, U+2029. Affected Versions Only `@xmldom/xmldom` 0.9.x is affected. The vulnerable matchers are built by `lib/grammar.js`'s `m`-flagged `reg()` builder, and the DocType `publicId`/`systemId` `requireWellFormed` check that consumes them was introduced in 0.9.10 (the GHSA-f6ww-3ggp-fr8h fix); 0.9.10 and 0.9.11 carry it. `0.8.x` performs the same `requireWellFormed` check with inline, non-`m` regular expressions and is not affected. The unscoped `xmldom` package has no `grammar.js` and no `requireWellFormed` serializer, so there is no check to bypass. Proof of Concept ```js const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom'); const impl = new DOMImplementation(); // publicId: complete literal on line 1, then newline + breakout const dt = impl.createDocumentType('html', '"valid pubid"\n">', ''); const doc = impl.createDocument(null, 'root', dt); console.log(new XMLSerializer().serializeToString(doc, { requireWellFormed: true })); // Observed (no throw): // > // Expected: InvalidStateError (publicId is not a valid PubidLiteral). // Control: a single-line invalid publicId ("no-surrounding-quotes<>") DOES throw InvalidStateError, // confirming the check is active and specifically bypassed by the line terminator. ``` Impact • Bypass of the GHSA-f6ww-3ggp-fr8h mitigation. Applications that adopted `requireWellFormed: true` to neutralize DocumentType injection remain exposed. • XML structure injection into the DOCTYPE, including injected markup / entity declarations after the public or system identifier. Fix Applied The anchored `PubidLiteral`/`SystemLiteral` validators used by the `requireWellFormed` serializer no longer treat an interior line terminator as satisfying the `$` anchor, so a `publicId` or `systemId` containing any ECMAScript `LineTerminator` (U+000A, U+000D, U+2028, U+2029) is rejected with `InvalidStateError`. Valid single-line identifiers serialize unchanged, and the default serialization path is unaffected. > ⚠ 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 impl = new DOMImplementation(); const dt = impl.createDocumentType('html', '"valid pubid"\n">', ''); const doc = impl.createDocument(null, 'root', dt); // Default path (requireWellFormed off) — unchanged, still emits verbatim: console.log(new XMLSerializer().serializeToString(doc)); // > // Opt-in path — now throws instead of emitting the breakout: new XMLSerializer().serializeToString(doc, { requireWellFormed: true }); // InvalidStateError: DocumentType publicId is not a valid PubidLiteral ``` Why the default stays verbatim The W3C DOM Parsing "require well-formed" flag defaults to false, and a browser `XMLSerializer` emits the DOCTYPE verbatim. Unconditionally throwing on a malformed `publicId`/`systemId` would be an unjustified breaking change to the default path, so the fix tightens only the opt-in `requireWellFormed` validator, matching browser and spec defaults. Residual limitation The guarantee holds only for callers that pass `{ requireWellFormed: true }`; the default serialization path still emits `publicId`/`systemId` verbatim. `publicId` and `systemId` are not validated at creation (`createDocumentType`) or on direct property assignment (`documentType.publicId = …`) — the WHATWG DOM specification places no well-formedness constraint on these fields at creation time, so the serializer is the spec-aligned enforcement point.

Related

Alias · CVE (1)

  • CVE-2026-83618 xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. vulnerability

Weakness · CWE (2)

  • CWE-625 Permissive Regular Expression weakness
  • CWE-91 XML Injection weakness

References