VYPR
High severityNVD Advisory· Published May 7, 2026· Updated May 7, 2026

CVE-2026-41674

CVE-2026-41674

Description

xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. In @xmldom/xmldom prior to versions 0.9.10 and 0.8.13 and xmldom version 0.6.0 and prior, the package serializes DocumentType node fields (internalSubset, publicId, systemId) verbatim without any escaping or validation. When these fields are set programmatically to attacker-controlled strings, XMLSerializer.serializeToString can produce output where the DOCTYPE declaration is terminated early and arbitrary markup appears outside it. This issue has been patched in versions @xmldom/xmldom versions 0.9.10 and 0.8.13.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@xmldom/xmldomnpm
< 0.8.130.8.13
@xmldom/xmldomnpm
>= 0.9.0, < 0.9.100.9.10
xmldomnpm
<= 0.6.0

Affected products

1

Patches

1
372008f9ae0e

fix: prevent XML injection via unsafe DocumentType serialization (GHSA-f6ww-3ggp-fr8h)

https://github.com/xmldom/xmldomkarfauApr 12, 2026via ghsa
7 files changed · +182 19
  • index.d.ts+48 7 modified
    @@ -1378,10 +1378,32 @@ declare module '@xmldom/xmldom' {
     	interface DocumentType extends Node {
     		/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentType/name) */
     		readonly name: string;
    +		/**
    +		 * The internal subset string (the raw content between `[` and `]`), or an empty string.
    +		 * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this — direct
    +		 * property writes succeed and the written value is serialized verbatim.
    +		 * When serialized with `requireWellFormed: true`, throws `InvalidStateError` if the value
    +		 * contains `"]>"`.
    +		 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentType/internalSubset)
    +		 */
     		readonly internalSubset: string;
    -		/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentType/publicId) */
    +		/**
    +		 * The external subset public identifier, stored verbatim including surrounding quotes.
    +		 * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this — direct
    +		 * property writes succeed and the written value is serialized verbatim.
    +		 * When serialized with `requireWellFormed: true`, throws `InvalidStateError` if the value
    +		 * is non-empty and does not match the XML `PubidLiteral` production (XML 1.0 [12]).
    +		 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentType/publicId)
    +		 */
     		readonly publicId: string;
    -		/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentType/systemId) */
    +		/**
    +		 * The external subset system identifier, stored verbatim including surrounding quotes.
    +		 * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this — direct
    +		 * property writes succeed and the written value is serialized verbatim.
    +		 * When serialized with `requireWellFormed: true`, throws `InvalidStateError` if the value
    +		 * is non-empty and does not match the XML `SystemLiteral` production (XML 1.0 [11]).
    +		 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DocumentType/systemId)
    +		 */
     		readonly systemId: string;
     	}
     
    @@ -1446,8 +1468,21 @@ declare module '@xmldom/xmldom' {
     		 */
     		createDocumentType(
     			qualifiedName: string,
    +			/**
    +			 * External subset public identifier. Stored verbatim including surrounding quotes.
    +			 * No creation-time validation — deferred to a future breaking release.
    +			 */
     			publicId?: string,
    -			systemId?: string
    +			/**
    +			 * External subset system identifier. Stored verbatim including surrounding quotes.
    +			 * No creation-time validation — deferred to a future breaking release.
    +			 */
    +			systemId?: string,
    +			/**
    +			 * Internal subset string (content between `[` and `]`). Stored verbatim.
    +			 * No creation-time validation — deferred to a future breaking release.
    +			 */
    +			internalSubset?: string
     		): DocumentType;
     
     		/**
    @@ -1528,14 +1563,20 @@ declare module '@xmldom/xmldom' {
     		 * `InvalidStateError` when `requireWellFormed` is `true` and any of the following conditions
     		 * hold:
     		 * - CDATASection data contains `"]]>"`
    -		 * - Text data contains characters outside the XML Char production - a Comment node's data
    -		 * contains `--` anywhere or ends with `-`
    +		 * - Text data contains characters outside the XML Char production
    +		 * - a Comment node's data contains `--` anywhere or ends with `-`
     		 * - a ProcessingInstruction's target contains `:` or is an ASCII case-insensitive match for
    -		 * `"xml"`, or its data contains `?>` or characters outside the XML Char production - the
    -		 * Document has no `documentElement`
    +		 * `"xml"`, or its data contains `?>` or characters outside the XML Char production
    +		 * - a DocumentType's `publicId` is non-empty and does not match the XML `PubidLiteral`
    +		 * production (W3C DOM Parsing §3.2.1.3; XML 1.0 production [12])
    +		 * - a DocumentType's `systemId` is non-empty and does not match the XML `SystemLiteral`
    +		 * production (W3C DOM Parsing §3.2.1.3; XML 1.0 production [11])
    +		 * - a DocumentType's `internalSubset` contains `"]>"`
    +		 * - the Document has no `documentElement`
     		 * @see https://developer.mozilla.org/docs/Web/API/XMLSerializer/serializeToString
     		 * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring
     		 * @see https://github.com/w3c/DOM-Parsing/issues/84
    +		 * @prettierignore
     		 */
     		serializeToString(
     			node: Node,
    
  • lib/dom.js+59 7 modified
    @@ -883,11 +883,21 @@ DOMImplementation.prototype = {
     	 * The {@link https://www.w3.org/TR/DOM-Level-3-Core/glossary.html#dt-qualifiedname qualified
     	 * name} of the document type to be created.
     	 * @param {string} [publicId]
    -	 * The external subset public identifier.
    +	 * The external subset public identifier. Stored verbatim including surrounding quotes.
    +	 * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
    +	 * if the value is non-empty and does not match the XML `PubidLiteral` production
    +	 * (W3C DOM Parsing §3.2.1.3; XML 1.0 production [12]). Creation-time validation is not
    +	 * enforced — deferred to a future breaking release.
     	 * @param {string} [systemId]
    -	 * The external subset system identifier.
    +	 * The external subset system identifier. Stored verbatim including surrounding quotes.
    +	 * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
    +	 * if the value is non-empty and does not match the XML `SystemLiteral` production
    +	 * (W3C DOM Parsing §3.2.1.3; XML 1.0 production [11]). Creation-time validation is not
    +	 * enforced — deferred to a future breaking release.
     	 * @param {string} [internalSubset]
    -	 * the internal subset or an empty string if it is not present
    +	 * The internal subset or an empty string if it is not present. Stored verbatim.
    +	 * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
    +	 * if the value contains `"]>"`. Creation-time validation is not enforced.
     	 * @returns {DocumentType}
     	 * A new {@link DocumentType} node with {@link Node#ownerDocument} set to null.
     	 * @throws {DOMException}
    @@ -2709,6 +2719,31 @@ CDATASection.prototype = {
     };
     _extends(CDATASection, Text);
     
    +/**
    + * @class DocumentType
    + * @augments Node
    + * @property {string} publicId
    + * The external subset public identifier, stored verbatim (including surrounding quotes).
    + * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this constraint —
    + * direct property writes succeed and the written value is serialized verbatim.
    + * When serialized with `requireWellFormed: true`, the serializer validates the value against
    + * the XML `PubidLiteral` production and throws `InvalidStateError` if it does not match.
    + * @property {string} systemId
    + * The external subset system identifier, stored verbatim (including surrounding quotes).
    + * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this constraint —
    + * direct property writes succeed and the written value is serialized verbatim.
    + * When serialized with `requireWellFormed: true`, the serializer validates the value against
    + * the XML `SystemLiteral` production and throws `InvalidStateError` if it does not match.
    + * @property {string} internalSubset
    + * The internal subset string (the raw content between `[` and `]`), or an empty string.
    + * Declared `readonly` by the WHATWG DOM spec; xmldom does not enforce this constraint —
    + * direct property writes succeed and the written value is serialized verbatim.
    + * When serialized with `requireWellFormed: true`, the serializer throws `InvalidStateError`
    + * if the value contains `"]>"`.
    + * @see https://developer.mozilla.org/en-US/docs/Web/API/DocumentType MDN
    + * @see https://dom.spec.whatwg.org/#interface-documenttype WHATWG DOM
    + * @prettierignore
    + */
     function DocumentType(symbol) {
     	checkSymbol(symbol);
     }
    @@ -2788,14 +2823,20 @@ function XMLSerializer() {}
      * With name `InvalidStateError` when `requireWellFormed` is `true` and any of the following
      * conditions hold:
      * - CDATASection data contains `"]]>"`
    - * - Text data contains characters outside the XML Char production - a Comment node's data
    - * contains `--` anywhere or ends with `-`
    + * - Text data contains characters outside the XML Char production
    + * - a Comment node's data contains `--` anywhere or ends with `-`
      * - a ProcessingInstruction's target contains `:` or is an ASCII case-insensitive match for
    - * `"xml"`, or its data contains `?>` or characters outside the XML Char production the
    - * Document has no `documentElement`.
    + * `"xml"`, or its data contains `?>` or characters outside the XML Char production
    + * - a DocumentType's `publicId` is non-empty and does not match the XML `PubidLiteral`
    + * production (W3C DOM Parsing §3.2.1.3; XML 1.0 production [12])
    + * - a DocumentType's `systemId` is non-empty and does not match the XML `SystemLiteral`
    + * production (W3C DOM Parsing §3.2.1.3; XML 1.0 production [11])
    + * - a DocumentType's `internalSubset` contains `"]>"`
    + * - the Document has no `documentElement`
      * @see https://developer.mozilla.org/docs/Web/API/XMLSerializer/serializeToString
      * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring
      * @see https://github.com/w3c/DOM-Parsing/issues/84
    + * @prettierignore
      */
     XMLSerializer.prototype.serializeToString = function (node, options) {
     	return nodeSerializeToString.call(node, options);
    @@ -3067,6 +3108,17 @@ function serializeToString(node, buf, visibleNamespaces, opts) {
     		case DOCUMENT_TYPE_NODE:
     			var pubid = node.publicId;
     			var sysid = node.systemId;
    +			if (requireWellFormed) {
    +				if (pubid && !g.PubidLiteral_match.test(pubid)) {
    +					throw new DOMException('The DocumentType publicId is not a valid PubidLiteral', 'InvalidStateError');
    +				}
    +				if (sysid && sysid !== '.' && !g.SystemLiteral_match.test(sysid)) {
    +					throw new DOMException('The DocumentType systemId is not a valid SystemLiteral', 'InvalidStateError');
    +				}
    +				if (node.internalSubset && node.internalSubset.indexOf(']>') !== -1) {
    +					throw new DOMException('The DocumentType internalSubset contains "]>"', 'InvalidStateError');
    +				}
    +			}
     			buf.push(g.DOCTYPE_DECL_START, ' ', node.name);
     			if (pubid) {
     				buf.push(' ', g.PUBLIC, ' ', pubid);
    
  • lib/grammar.js+8 0 modified
    @@ -402,6 +402,12 @@ var ExternalID_match = reg(
     		regg(PUBLIC, S, '(?<PubidLiteral>', PubidLiteral, ')', S, '(?<SystemLiteral>', SystemLiteral, ')')
     	)
     );
    +// Full-string anchored matcher for requireWellFormed serializer checks
    +// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-document-node
    +var PubidLiteral_match = reg('^', PubidLiteral, '$');
    +// Full-string anchored matcher for requireWellFormed serializer checks
    +// https://w3c.github.io/DOM-Parsing/#xml-serializing-a-document-node
    +var SystemLiteral_match = reg('^', SystemLiteral, '$');
     
     // https://www.w3.org/TR/xml11/#NT-NDataDecl
     // `[76] NDataDecl ::= S 'NDATA' S Name` [VC: Notation Declared]
    @@ -525,6 +531,7 @@ exports.PEReference = PEReference;
     exports.PI = PI;
     exports.PUBLIC = PUBLIC;
     exports.PubidLiteral = PubidLiteral;
    +exports.PubidLiteral_match = PubidLiteral_match;
     exports.QName = QName;
     exports.QName_exact = QName_exact;
     exports.QName_group = QName_group;
    @@ -533,6 +540,7 @@ exports.SChar_s = SChar_s;
     exports.S_OPT = S_OPT;
     exports.SYSTEM = SYSTEM;
     exports.SystemLiteral = SystemLiteral;
    +exports.SystemLiteral_match = SystemLiteral_match;
     exports.InvalidChar = InvalidChar;
     exports.UNICODE_REPLACEMENT_CHARACTER = UNICODE_REPLACEMENT_CHARACTER;
     exports.UNICODE_SUPPORT = UNICODE_SUPPORT;
    
  • test/dom/serializer.test.js+43 0 modified
    @@ -428,6 +428,49 @@ describe('XMLSerializer.serializeToString', () => {
     				expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError');
     			});
     		});
    +
    +		describe('DocumentType', () => {
    +			test('default: DocumentType with invalid publicId serializes verbatim — no throw', () => {
    +				const doctype = new DOMImplementation().createDocumentType('name', '"invalid<char"', '');
    +				const dtDoc = new DOMImplementation().createDocument(null, 'root', doctype);
    +				expect(() => new XMLSerializer().serializeToString(dtDoc)).not.toThrow();
    +			});
    +
    +			test('requireWellFormed: true on DocumentType with invalid publicId throws InvalidStateError', () => {
    +				const doctype = new DOMImplementation().createDocumentType('name', '"invalid<char"', '');
    +				const dtDoc = new DOMImplementation().createDocument(null, 'root', doctype);
    +				expectDOMException(() => new XMLSerializer().serializeToString(dtDoc, { requireWellFormed: true }), 'InvalidStateError');
    +			});
    +
    +			test('requireWellFormed: true on DocumentType with invalid systemId throws InvalidStateError', () => {
    +				const doctype = new DOMImplementation().createDocumentType('name', '', 'no-quotes-around-this');
    +				const dtDoc = new DOMImplementation().createDocument(null, 'root', doctype);
    +				expectDOMException(() => new XMLSerializer().serializeToString(dtDoc, { requireWellFormed: true }), 'InvalidStateError');
    +			});
    +
    +			test('requireWellFormed: true on DocumentType with "]>" in internalSubset throws InvalidStateError', () => {
    +				const doctype = new DOMImplementation().createDocumentType('name', '', '', ']><injected/>');
    +				const dtDoc = new DOMImplementation().createDocument(null, 'root', doctype);
    +				expectDOMException(() => new XMLSerializer().serializeToString(dtDoc, { requireWellFormed: true }), 'InvalidStateError');
    +			});
    +
    +			test('requireWellFormed: true on DocumentType with valid fields does not throw', () => {
    +				const doctype = new DOMImplementation().createDocumentType(
    +					'html',
    +					'"-//W3C//DTD HTML 4.01//EN"',
    +					'"http://www.w3.org/TR/html4/strict.dtd"'
    +				);
    +				const dtDoc = new DOMImplementation().createDocument(null, 'root', doctype);
    +				expect(() => new XMLSerializer().serializeToString(dtDoc, { requireWellFormed: true })).not.toThrow();
    +			});
    +
    +			test('direct property write: setting invalid publicId then requireWellFormed: true throws InvalidStateError', () => {
    +				const doctype = new DOMImplementation().createDocumentType('name', '"-//W3C//DTD//EN"', '');
    +				const dtDoc = new DOMImplementation().createDocument(null, 'root', doctype);
    +				doctype.publicId = '"invalid<char"';
    +				expectDOMException(() => new XMLSerializer().serializeToString(dtDoc, { requireWellFormed: true }), 'InvalidStateError');
    +			});
    +		});
     	});
     });
     
    
  • test/grammar/externalid.test.js+20 5 modified
    @@ -1,10 +1,21 @@
     'use strict';
     
     const { describe, expect, test } = require('@jest/globals');
    -const { ExternalID, PubidLiteral, S, SystemLiteral, reg, NotationDecl, Name, ExternalID_match } = require('../../lib/grammar');
    +const {
    +	ExternalID,
    +	PubidLiteral,
    +	PubidLiteral_match,
    +	S,
    +	SystemLiteral,
    +	SystemLiteral_match,
    +	reg,
    +	NotationDecl,
    +	Name,
    +	ExternalID_match,
    +} = require('../../lib/grammar');
     const { range } = require('./utils');
     
    -describe('SystemLiteral', () => {
    +describe('SystemLiteral and SystemLiteral_match', () => {
     	[
     		'""',
     		"''",
    @@ -15,25 +26,29 @@ describe('SystemLiteral', () => {
     	].forEach((valid) =>
     		test(`should match ${valid}`, () => {
     			expect(SystemLiteral.exec(valid)[0]).toBe(valid);
    +			expect(SystemLiteral_match.test(valid)).toBe(true);
     		})
     	);
    -	['', '"""', "'''"].forEach((invalid) =>
    +	['', '"""', "'''", '"url" SYSTEM "injected"', "'url' SYSTEM 'injected'"].forEach((invalid) =>
     		test(`should not match ${invalid}`, () => {
     			expect(reg('^', SystemLiteral, '$').test(invalid)).toBe(false);
    +			expect(SystemLiteral_match.test(invalid)).toBe(false);
     		})
     	);
     });
     
    -describe('PubidLiteral', () => {
    +describe('PubidLiteral and PubidLiteral_match', () => {
     	['""', "''", '"\'"', `"\x20\x0D\x0Aa-zA-Z0-9-'()+,./:=?;!*#@$_%"`, `'\x20\x0D\x0Aa-zA-Z0-9-()+,./:=?;!*#@$_%'`].forEach(
     		(valid) =>
     			test(`should match ${valid}`, () => {
     				expect(PubidLiteral.exec(valid)[0]).toBe(valid);
    +				expect(PubidLiteral_match.test(valid)).toBe(true);
     			})
     	);
    -	['', '"""', "'\"'", "'''"].forEach((invalid) =>
    +	['', '"""', "'\"'", "'''", '"invalid<char"', "'invalid<char'"].forEach((invalid) =>
     		test(`should not match ${invalid}`, () => {
     			expect(reg('^', PubidLiteral, '$').test(invalid)).toBe(false);
    +			expect(PubidLiteral_match.test(invalid)).toBe(false);
     		})
     	);
     });
    
  • test/grammar/regexp.js+2 0 modified
    @@ -3,11 +3,13 @@
     const S = /[\x20\x09\x0D\x0A]+/mu;
     const S_OPT = /[\x20\x09\x0D\x0A]*/mu;
     const SystemLiteral = /(?:"[^"]*"|'[^']*')/mu;
    +const SystemLiteral_match = /^(?:"[^"]*"|'[^']*')$/mu;
     const ABOUT_LEGACY_COMPAT_SystemLiteral = /(?:"about:legacy-compat"|'about:legacy-compat')/mu;
     const Char = /[-\x09\x0A\x0D\x20-\x2C\x2E-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/mu;
     const InvalidChar = /[^-\x09\x0A\x0D\x20-\x2C\x2E-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/u;
     const CDSect = /<!\[CDATA\[[-\x09\x0A\x0D\x20-\x2C\x2E-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]*?\]\]>/mu;
     const PubidLiteral = /(?:"[\x20\x0D\x0Aa-zA-Z0-9-'()+,.\/:=?;!*#@$_%]*"|'[\x20\x0D\x0Aa-zA-Z0-9-()+,.\/:=?;!*#@$_%]*')/mu;
    +const PubidLiteral_match = /^(?:"[\x20\x0D\x0Aa-zA-Z0-9-'()+,.\/:=?;!*#@$_%]*"|'[\x20\x0D\x0Aa-zA-Z0-9-()+,.\/:=?;!*#@$_%]*')$/mu;
     const Comment = /<!--(?:[\x09\x0A\x0D\x20-\x2C\x2E-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]|-[\x09\x0A\x0D\x20-\x2C\x2E-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}])*-->/mu;
     const ExternalID = /(?:(?:SYSTEM[\x20\x09\x0D\x0A]+(?:"[^"]*"|'[^']*'))|(?:PUBLIC[\x20\x09\x0D\x0A]+(?:"[\x20\x0D\x0Aa-zA-Z0-9-'()+,.\/:=?;!*#@$_%]*"|'[\x20\x0D\x0Aa-zA-Z0-9-()+,.\/:=?;!*#@$_%]*')[\x20\x09\x0D\x0A]+(?:"[^"]*"|'[^']*')))/mu;
     const ExternalID_match = /^(?:(?:SYSTEM[\x20\x09\x0D\x0A]+(?<SystemLiteralOnly>(?:"[^"]*"|'[^']*')))|(?:PUBLIC[\x20\x09\x0D\x0A]+(?<PubidLiteral>(?:"[\x20\x0D\x0Aa-zA-Z0-9-'()+,.\/:=?;!*#@$_%]*"|'[\x20\x0D\x0Aa-zA-Z0-9-()+,.\/:=?;!*#@$_%]*'))[\x20\x09\x0D\x0A]+(?<SystemLiteral>(?:"[^"]*"|'[^']*'))))/mu;
    
  • test/grammar/__snapshots__/regexp.test.js.snap+2 0 modified
    @@ -5,11 +5,13 @@ exports[`all grammar regular expressions should have the expected keys 1`] = `
       "S",
       "S_OPT",
       "SystemLiteral",
    +  "SystemLiteral_match",
       "ABOUT_LEGACY_COMPAT_SystemLiteral",
       "Char",
       "InvalidChar",
       "CDSect",
       "PubidLiteral",
    +  "PubidLiteral_match",
       "Comment",
       "ExternalID",
       "ExternalID_match",
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

1