# xmldom: requireWellFormed DocType publicId/systemId validation is bypassable via an embedded line terminator

In @xmldom/xmldom 0.9.10-0.9.11, an m-flagged regex lets a newline in DocumentType publicId bypass requireWellFormed, injecting DOCTYPE markup.

- Payout: Research
- Severity: high
- Program: @xmldom/xmldom
- Found by: karfau
- Disclosed: 2026-09-08
- CVE: CVE-2026-83618
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-vr34-hp96-76pp
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vr34-hp96-76pp
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:parser-differential technique:source-review vuln:xxe

**Target:** XMLSerializer DocumentType publicId/systemId validation (npm library)
**Root cause:** PubidLiteral/SystemLiteral validators are built with a shared reg() builder that adds the `m` flag, so `^…$` anchor per-line; a valid literal on the first line passes .test() even with a line terminator and breakout markup following.
**Steps:**
1. `const dt = impl.createDocumentType('html', '"valid pubid"\n"><!ENTITY xxe SYSTEM "file:///etc/passwd">', '')`
2. `const doc = impl.createDocument(null, 'root', dt)`
3. `new XMLSerializer().serializeToString(doc, { requireWellFormed: true })`
4. Observe no throw; output emits `<!DOCTYPE html PUBLIC "valid pubid"` then newline and the injected `<!ENTITY …>` markup
5. Control: single-line invalid publicId `"no-surrounding-quotes<>"` throws InvalidStateError, confirming the check is active but bypassed by the line terminator
**How found:** Noticed the shared reg() builder compiles anchored productions with the `m` flag, making `^…$` line anchors; hypothesized a full-string validator using .test() would accept a string with one conforming line, then tried a valid literal followed by newline and breakout markup.
**Exploitation:** Attacker controls a DocumentType publicId/systemId (e.g. via untrusted DOM content); app serializes with requireWellFormed:true expecting neutralization, but the newline-terminated value emits injected entity declarations/markup into the DOCTYPE, enabling XXE-style injection downstream.
**Escalation:** Chains onto the GHSA-f6ww-3ggp-fr8h mitigation: apps that adopted requireWellFormed to stop DocumentType injection remain exposed, so injected entities can reach downstream parsers.
**Insight:** When auditing regex-based validators, check whether the shared builder adds `m`/`s` flags: `^…$` under `m` accepts any string with one valid line, so a validator meant to be full-string can be bypassed by appending a line terminator (U+000A/000D/2028/2029) plus payload.

