CVE-2026-83608

CVE-2026-83608 is a high-severity security vulnerability in @xmldom/xmldom (npm), affecting versions >= 0.7.0, <= 0.8.14. It is fixed in 0.8.15, 0.9.12.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

xmldom: DocType name Injection Bypasses requireWellFormed

The @xmldom/xmldom serializer emits DocumentType.name verbatim into the
<!DOCTYPE …> declaration with no well-formedness guard. GHSA-f6ww-3ggp-fr8h
(CVE-2026-41674) hardened the serializer's requireWellFormed path for a
DocumentType's sibling fields, publicId, systemId, and internalSubset ,
but it did not add any check for name. A > (or whitespace) in the name
terminates the doctype declaration early, letting the remaining characters
become sibling markup in the serialized output.

Because requireWellFormed: true, the recommended mitigation for the prior
xmldom injection CVEs, performs no validation on the DocType name, this is a
bypass of that control, in the same family as the open element-name
(GHSA-w2rr-34g9-rvrj) and attribute-name (GHSA-4w3w-2rp5-g8jm) name-injection advisories.

Details

The serializer's DOCUMENT_TYPE_NODE case runs the requireWellFormed block
only against publicId, systemId, and internalSubset, then pushes
n.name directly into the buffer between the <!DOCTYPE prefix and the
closing >:

Enabling write paths

DocumentType.name is a plain, writable own-property, so the enabling vector
differs by line:

  • 0.9.x, createDocumentType() validates the name via
    validateQualifiedName (lib/dom.js#L925-L936,
    validation at #L926),
    so the deliverable vector is a direct property write
    (dt.name = 'html><script>…') to the unguarded own-property.
  • 0.8.x, createDocumentType() does not validate the name
    (lib/dom.js#L456-L464),
    so the malicious name is reachable directly through createDocumentType() as
    well as via direct property write.
  • unscoped xmldom (<= 0.6.0), createDocumentType() does not validate
    the name (lib/dom.js#L286),
    same as 0.8.x.

This is the same structural root cause the sibling name-injection advisories
share: the serializer's requireWellFormed path validates content delimiters
but no name field, and every name-like field is a plain writable property, so
mutation / direct property-write bypasses any creation-time check.

Root Cause

  1. The serializer's requireWellFormed DocType block checks publicId,
    systemId, and internalSubset (the fields hardened by GHSA-f6ww-3ggp-fr8h)
    but has no check for name.
  2. DocumentType.name is a plain writable own-property; on 0.8.x and the
    unscoped package createDocumentType() does not validate it either.
  3. The serializer emits name directly between the doctype delimiters:
    <!DOCTYPE ${name}…>.

Proof of Concept

Run against @xmldom/xmldom v0.9.10 (commit bb7a085):

const { DOMImplementation, XMLSerializer, DOMParser } = require('@xmldom/xmldom');

const impl = new DOMImplementation();
const serializer = new XMLSerializer();

// 0.9.x createDocumentType validates the name, so overwrite it via direct property write
const dt = impl.createDocumentType('html', '', '');
dt.name = 'html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script';
const doc = impl.createDocument(null, 'r', dt);

const output = serializer.serializeToString(doc, { requireWellFormed: true });
console.log(output);
// Output: <!DOCTYPE html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><r/>
//
// requireWellFormed: true did NOT prevent the injection (no exception thrown).
// The injected <script> is well-formed XHTML that a browser would execute.

Confirmed runtime behavior:

  • 0.9.x, createDocumentType() rejects the malicious name at creation
    (InvalidCharacterError); a direct write to dt.name bypasses that, and
    serializeToString(…, { requireWellFormed: true }) emits the breakout with no
    exception.
  • Re-parse confirmation, re-parsing the output shows the injected
    <script> is a real second top-level element originating entirely from the
    DocType name: the parser rejects it with
    HierarchyRequestError: Only one element can be added and only after doctype.
    A comment-injection variant (dt.name = 'html><!--INJECTED--', output
    <!DOCTYPE html><!--INJECTED--><r/>) re-parses cleanly and the injected
    comment node is enumerable, confirming the injected node is structurally live.
  • 0.8.x (v0.8.13, e5c1480), createDocumentType('html><script>…', '', '')
    accepts the malicious name directly (no creation-time validation), and
    serializeToString(doc, null, null, { requireWellFormed: true }) produces
    <!DOCTYPE html><script>alert(1)</script><r/> with no exception.

A browser reproduction does not apply: browsers keep DocumentType.name
readonly, so the direct-write vector cannot be reproduced in a browser DOM.
The injection is specific to xmldom exposing name as writable and serializing
it without a guard.

Fix Applied

Under requireWellFormed, the serializer validates the DocType name as a well-formed XML
Name and throws InvalidStateError when it is not, matching the sibling
publicId/systemId/internalSubset checks. Non-breaking and opt-in; ships on both maintained versions. No
creation-time change is made: 0.9.x already validates the name at createDocumentType, and the
0.8.x/unscoped creation gap cannot be closed without a breaking change, so it is left
unfixed. See the XML Name production.

⚠ 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

const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');

const impl = new DOMImplementation();
const serializer = new XMLSerializer();

const dt = impl.createDocumentType('html', '', '');
dt.name = 'html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script';
const doc = impl.createDocument(null, 'r', dt);

// Default path: the ill-formed name is still emitted verbatim (injection present).
console.log(serializer.serializeToString(doc));
// <!DOCTYPE html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><r/>

// Opt-in path: serialization throws instead of emitting the breakout.
serializer.serializeToString(doc, { requireWellFormed: true });
// throws InvalidStateError

Why the default stays verbatim

W3C DOM Parsing's require-well-formed flag defaults to false, and the browser
XMLSerializer emits the name verbatim in that default mode. Unconditionally
throwing would be an unjustified breaking change against that specified default,
so the guard is opt-in behind { requireWellFormed: true }.

Residual limitation

The default serialization path still emits the ill-formed DocType name
verbatim; protection applies only when requireWellFormed: true is passed. No
creation-time validation is added for the DocType name: 0.9.x already validates
at createDocumentType, and the 0.8.x/unscoped creation gap is left unfixed, it
cannot be closed without a breaking change.

Impact

Applications that build a DocumentType node with an attacker-influenced
name, via direct property write on any affected line, or via
createDocumentType() on 0.8.x and the unscoped package, and serialize the
document are vulnerable to XML/markup injection:

  • XML structure injection, breaking out of the <!DOCTYPE …> declaration
    to inject arbitrary sibling elements, comments, or additional markup into the
    output.
  • XSS via XHTML, if the serialized output is served as XHTML or processed
    by a browser-based XML parser, an injected <script> element (in the XHTML
    namespace) executes.
  • requireWellFormed bypass, applications that adopted
    requireWellFormed: true as a mitigation for the prior injection CVEs
    (including the sibling DocType fields fixed by GHSA-f6ww-3ggp-fr8h) remain
    vulnerable through the DocType name.

Affected versions

@xmldom/xmldom (>= 0.7.0, <= 0.8.14) @xmldom/xmldom (>= 0.9.0, <= 0.9.11) xmldom (<= 0.6.0)

Security releases

@xmldom/xmldom → 0.8.15 (npm) @xmldom/xmldom → 0.9.12 (npm)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade the following packages to resolve this vulnerability:

@xmldom/xmldom to 0.8.15 or later; @xmldom/xmldom to 0.9.12 or later

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is CVE-2026-83608? CVE-2026-83608 is a high-severity security vulnerability in @xmldom/xmldom (npm), affecting versions >= 0.7.0, <= 0.8.14. It is fixed in 0.8.15, 0.9.12.
  2. Which packages are affected by CVE-2026-83608?
    • @xmldom/xmldom (npm) (versions >= 0.7.0, <= 0.8.14)
    • xmldom (npm) (versions <= 0.6.0)
  3. Is there a fix for CVE-2026-83608? Yes. CVE-2026-83608 is fixed in 0.8.15, 0.9.12. Upgrade to this version or later.
  4. Is CVE-2026-83608 exploitable, and should I be worried? Whether CVE-2026-83608 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  5. What actually determines whether CVE-2026-83608 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  6. How do I fix CVE-2026-83608?
    • Upgrade @xmldom/xmldom to 0.8.15 or later
    • Upgrade @xmldom/xmldom to 0.9.12 or later

Stop the waste.
Protect your environment with Kodem.