GHSA-vr34-hp96-76pp
xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator
Quick fix
GHSA-vr34-hp96-76pp — @xmldom/xmldom: upgrade to the fixed version with the command below.
npm install @xmldom/xmldom@0.9.12Details
## 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.
Are you affected?
Enter the version of the package you're using.
Affected packages
References
- https://github.com/xmldom/xmldom/security/advisories/GHSA-vr34-hp96-76pp[WEB]
- https://nvd.nist.gov/vuln/detail/CVE-2026-83618[ADVISORY]
- https://github.com/xmldom/xmldom/pull/1071[WEB]
- https://github.com/xmldom/xmldom/commit/7b2ec67e1750daadd0bb06c92e875e726544a362[WEB]
- https://github.com/xmldom/xmldom[PACKAGE]
- https://github.com/xmldom/xmldom/releases/tag/0.9.12[WEB]