CVE-2026-41675
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 allows attacker-controlled processing instruction data to be serialized into XML without validating or neutralizing the PI-closing sequence ?>. As a result, an attacker can terminate the processing instruction early and inject arbitrary XML nodes into the serialized output. 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.
| Package | Affected versions | Patched versions |
|---|---|---|
@xmldom/xmldomnpm | < 0.8.13 | 0.8.13 |
@xmldom/xmldomnpm | >= 0.9.0, < 0.9.10 | 0.9.10 |
xmldomnpm | <= 0.6.0 | — |
Affected products
1Patches
17207a4b0e0bcfix: prevent XML injection via unsafe PI serialization (GHSA-x6wf-f3px-wcqx)
4 files changed · +144 −12
index.d.ts+23 −8 modified@@ -1267,12 +1267,21 @@ declare module '@xmldom/xmldom' { createEntityReference(name: string): EntityReference; /** - * Returns a ProcessingInstruction node whose target is target and data is data. If target does - * not match the Name production an "InvalidCharacterError" DOMException will be thrown. If - * data contains "?>" an "InvalidCharacterError" DOMException will be thrown. + * Returns a ProcessingInstruction node whose target is target and data is data. * - * [MDN - * Reference](https://developer.mozilla.org/docs/Web/API/Document/createProcessingInstruction) + * __This behavior is slightly different from the in the specs__: + * - it does not do any input validation on the arguments and doesn't throw + * "InvalidCharacterError". + * + * Note: When the resulting document is serialized with `requireWellFormed: true`, the + * serializer throws `InvalidStateError` if `.target` contains `:` or is an ASCII + * case-insensitive match for `"xml"`, or if `.data` contains `?>` or characters outside the + * XML Char production (W3C DOM Parsing §3.2.1.7). Without that option the data is emitted + * verbatim. + * + * @see https://developer.mozilla.org/docs/Web/API/Document/createProcessingInstruction + * @see https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction + * @see https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml §3.2.1.7 */ createProcessingInstruction( target: string, @@ -1516,9 +1525,15 @@ declare module '@xmldom/xmldom' { * breaking milestone. * * @throws {DOMException} - * `InvalidStateError` when `requireWellFormed` is `true` and CDATASection data contains - * `"]]>"`, Text data contains characters outside the XML Char production, or the Document - * has no `documentElement`. + * `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 `-` + * - 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` + * @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 */
lib/dom.js+37 −3 modified@@ -2259,9 +2259,24 @@ Document.prototype = { return node; }, /** + * Returns a ProcessingInstruction node whose target is target and data is data. + * + * __This behavior is slightly different from the in the specs__: + * - it does not do any input validation on the arguments and doesn't throw + * "InvalidCharacterError". + * + * Note: When the resulting document is serialized with `requireWellFormed: true`, the + * serializer throws `InvalidStateError` if `.target` contains `:` or is an ASCII + * case-insensitive match for `"xml"`, or if `.data` contains `?>` or characters outside the + * XML Char production (W3C DOM Parsing §3.2.1.7). Without that option the data is emitted + * verbatim. + * * @param {string} target * @param {string} data * @returns {ProcessingInstruction} + * @see https://developer.mozilla.org/docs/Web/API/Document/createProcessingInstruction + * @see https://dom.spec.whatwg.org/#dom-document-createprocessinginstruction + * @see https://www.w3.org/TR/DOM-Parsing/#dfn-concept-serialize-xml §3.2.1.7 */ createProcessingInstruction: function (target, data) { var node = new ProcessingInstruction(PDC); @@ -2771,9 +2786,14 @@ function XMLSerializer() {} * @returns {string} * @throws {DOMException} * 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 `-`; - * the Document has no `documentElement`. + * conditions hold: + * - CDATASection data contains `"]]>"` + * - 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`. + * @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 */ @@ -3062,6 +3082,20 @@ function serializeToString(node, buf, visibleNamespaces, opts) { buf.push('>'); return; case PROCESSING_INSTRUCTION_NODE: + if (requireWellFormed) { + if (node.target.indexOf(':') !== -1 || node.target.toLowerCase() === 'xml') { + throw new DOMException('The ProcessingInstruction target is not well-formed', 'InvalidStateError'); + } + if (g.InvalidChar.test(node.data)) { + throw new DOMException( + 'The ProcessingInstruction data contains characters outside the XML Char production', + 'InvalidStateError' + ); + } + if (node.data.indexOf('?>') !== -1) { + throw new DOMException('The ProcessingInstruction data contains "?>"', 'InvalidStateError'); + } + } return buf.push('<?', node.target, ' ', node.data, '?>'); case ENTITY_REFERENCE_NODE: return buf.push('&', node.nodeName, ';');
test/dom/processing-instruction.test.js+9 −1 modified@@ -2,7 +2,8 @@ const { DOMParser, XMLSerializer } = require('../../lib'); const { MIME_TYPE } = require('../../lib/conventions'); -const { Node, ProcessingInstruction } = require('../../lib/dom'); +const { DOMImplementation, Node, ProcessingInstruction } = require('../../lib/dom'); +const { expectDOMException } = require('../errors/expectDOMException'); describe('ProcessingInstruction', () => { describe('constructor', () => { @@ -51,4 +52,11 @@ describe('ProcessingInstruction', () => { pi.data = 'href="newcss.css" type="text/css"'; expect(pi.data).toBe('href="newcss.css" type="text/css"'); }); + + test('createProcessingInstruction accepts any target or data without throwing', () => { + const doc = new DOMParser().parseFromString('<xml></xml>', MIME_TYPE.XML_TEXT); + expect(() => doc.createProcessingInstruction('ns:bad', 'data')).not.toThrow(); + expect(() => doc.createProcessingInstruction('xml', '?>')).not.toThrow(); + expect(() => doc.createProcessingInstruction('foo', 'inject?>evil')).not.toThrow(); + }); });
test/dom/serializer.test.js+75 −0 modified@@ -353,6 +353,81 @@ describe('XMLSerializer.serializeToString', () => { expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); }); }); + + describe('ProcessingInstruction', () => { + test('default: PI with ":" in target emits verbatim — no throw', () => { + const pi = doc.createProcessingInstruction('ns:target', 'data'); + doc.documentElement.appendChild(pi); + expect(() => new XMLSerializer().serializeToString(doc)).not.toThrow(); + }); + + test('default: PI with target "xml" emits verbatim — no throw', () => { + const pi = doc.createProcessingInstruction('xml', 'version="1.0"'); + doc.documentElement.appendChild(pi); + expect(() => new XMLSerializer().serializeToString(doc)).not.toThrow(); + }); + + test('default: PI with invalid XML Char in data emits verbatim — no throw', () => { + const pi = doc.createProcessingInstruction('foo', 'data\x00here'); + doc.documentElement.appendChild(pi); + expect(() => new XMLSerializer().serializeToString(doc)).not.toThrow(); + }); + + test('default: PI with "?>" in data emits verbatim — no throw', () => { + const pi = doc.createProcessingInstruction('foo', 'inject?>evil'); + doc.documentElement.appendChild(pi); + expect(new XMLSerializer().serializeToString(doc)).toBe('<root><?foo inject?>evil?></root>'); + }); + + test('requireWellFormed: true on PI with ":" in target throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('ns:target', 'data'); + doc.documentElement.appendChild(pi); + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + + test('requireWellFormed: true on PI with target "xml" throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('xml', 'version="1.0"'); + doc.documentElement.appendChild(pi); + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + + test('requireWellFormed: true on PI with target "XML" (uppercase) throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('XML', 'data'); + doc.documentElement.appendChild(pi); + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + + test('requireWellFormed: true on PI with target "Xml" (mixed case) throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('Xml', 'data'); + doc.documentElement.appendChild(pi); + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + + test('requireWellFormed: true on PI with invalid XML Char (\\x00) in data throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('foo', 'data\x00here'); + doc.documentElement.appendChild(pi); + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + + test('requireWellFormed: true on PI with "?>" in data throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('foo', 'inject?>evil'); + doc.documentElement.appendChild(pi); + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + + test('requireWellFormed: true on PI with clean target and data does not throw', () => { + const pi = doc.createProcessingInstruction('xml-stylesheet', 'href="style.css"'); + doc.documentElement.appendChild(pi); + expect(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true })).not.toThrow(); + }); + + test('mutation vector: set PI data to "?>" then requireWellFormed: true throws InvalidStateError', () => { + const pi = doc.createProcessingInstruction('foo', 'clean'); + doc.documentElement.appendChild(pi); + pi.data = 'inject?>evil'; + expectDOMException(() => new XMLSerializer().serializeToString(doc, { requireWellFormed: true }), 'InvalidStateError'); + }); + }); }); });
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- github.com/advisories/GHSA-x6wf-f3px-wcqxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-41675ghsaADVISORY
- github.com/xmldom/xmldom/commit/7207a4b0e0bcc228868075ed991665ef9f73b1c2nvdWEB
- github.com/xmldom/xmldom/releases/tag/0.8.13nvdWEB
- github.com/xmldom/xmldom/releases/tag/0.9.10nvdWEB
- github.com/xmldom/xmldom/security/advisories/GHSA-x6wf-f3px-wcqxnvdWEB
News mentions
1- Patch Tuesday - May 2026Rapid7 Blog · May 13, 2026