GitHub Advisoryadvisory · npm
GHSA-vr34-hp96-76pp xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator
HIGH
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
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