GHSA-vr34-hp96-76pp

    Dashboard / Vulnerabilities / GHSA-vr34-hp96-76pp

    GHSA-vr34-hp96-76pp

    Published: 8 Sept 2026Last Modified: 8 Sept 2026

    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.

    Affected packages

    Package

    Name: @xmldom/xmldom

    Purl: pkg:npm/%40xmldom/xmldom

    Affected ranges

    Type: SEMVER

    Events:

    Introduced- 0.9.10
    Fixed -0.9.12

    Affected versions

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High