GHSA-vr34-hp96-76pp
Dashboard / Vulnerabilities / GHSA-vr34-hp96-76pp
Summary: xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator
Details: ## 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 `<!DOCTYPE …>` 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"><!ENTITY …>` 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"><!ENTITY xxe SYSTEM "file:///etc/passwd">', ''); const doc = impl.createDocument(null, 'root', dt); console.log(new XMLSerializer().serializeToString(doc, { requireWellFormed: true })); // Observed (no throw): // <!DOCTYPE html PUBLIC "valid pubid" // "><!ENTITY xxe SYSTEM "file:///etc/passwd">><root/> // 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"><!ENTITY xxe SYSTEM "file:///etc/passwd">', ''); const doc = impl.createDocument(null, 'root', dt); // Default path (requireWellFormed off) — unchanged, still emits verbatim: console.log(new XMLSerializer().serializeToString(doc)); // <!DOCTYPE html PUBLIC "valid pubid" // "><!ENTITY xxe SYSTEM "file:///etc/passwd">><root/> // 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.
References: https://github.com/xmldom/xmldom/security/advisories/GHSA-vr34-hp96-76pp, https://nvd.nist.gov/vuln/detail/CVE-2026-83618, https://github.com/xmldom/xmldom/pull/1071, https://github.com/xmldom/xmldom/commit/7b2ec67e1750daadd0bb06c92e875e726544a362, https://github.com/xmldom/xmldom, https://github.com/xmldom/xmldom/releases/tag/0.9.12
Affected packages
Package
Name: @xmldom/xmldom
Purl: pkg:npm/%40xmldom/xmldom
Affected ranges
Type: SEMVER
Events:
