CVE-2026-34601
Description
xmldom is a pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module. In xmldom versions 0.6.0 and prior and @xmldom/xmldom prior to versions 0.8.12 and 0.9.9, xmldom/xmldom allows attacker-controlled strings containing the CDATA terminator ]]> to be inserted into a CDATASection node. During serialization, XMLSerializer emitted the CDATA content verbatim without rejecting or safely splitting the terminator. As a result, data intended to remain text-only became active XML markup in the serialized output, enabling XML structure injection and downstream business-logic manipulation. This issue has been patched in xmldom version 0.6.0 and @xmldom/xmldom versions 0.8.12 and 0.9.9.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
xmldomnpm | <= 0.6.0 | — |
@xmldom/xmldomnpm | < 0.8.12 | 0.8.12 |
@xmldom/xmldomnpm | >= 0.9.0, < 0.9.9 | 0.9.9 |
Affected products
1Patches
12b852e836ab8fix: XML injection via unsafe CDATA serialization (GHSA-wh4c-j3r5-mjhp) (#969)
6 files changed · +203 −7
CHANGELOG.md+32 −0 modified@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.9](https://github.com/xmldom/xmldom/compare/0.9.8...0.9.9) + +### Fixed + +- Security: `createCDATASection` now throws `InvalidCharacterError` when `data` contains `"]]>"`, as required by the [WHATWG DOM spec](https://dom.spec.whatwg.org/#dom-document-createcdatasection). [`GHSA-wh4c-j3r5-mjhp`](https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp) +- Security: `XMLSerializer` now splits CDATASection nodes whose data contains `"]]>"` into adjacent CDATA sections at serialization time, preventing XML injection via mutation methods (`appendData`, `replaceData`, `.data =`, `.textContent =`). [`GHSA-wh4c-j3r5-mjhp`](https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp) + +Code that passes a string containing `"]]>"` to `createCDATASection` and relied on the previously unsafe behavior will now receive `InvalidCharacterError`. Use a mutation method such as `appendData` if you intentionally need `"]]>"` in a CDATASection node's data. + +Thank you, [@thesmartshadow](https://github.com/thesmartshadow), for your contributions + + +## [0.8.12](https://github.com/xmldom/xmldom/compare/0.8.11...0.8.12) + +### Fixed + +- Security: `createCDATASection` now throws `InvalidCharacterError` when `data` contains `"]]>"`, as required by the [WHATWG DOM spec](https://dom.spec.whatwg.org/#dom-document-createcdatasection). [`GHSA-wh4c-j3r5-mjhp`](https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp) +- Security: `XMLSerializer` now splits CDATASection nodes whose data contains `"]]>"` into adjacent CDATA sections at serialization time, preventing XML injection via mutation methods (`appendData`, `replaceData`, `.data =`, `.textContent =`). [`GHSA-wh4c-j3r5-mjhp`](https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp) + +Code that passes a string containing `"]]>"` to `createCDATASection` and relied on the previously unsafe behavior will now receive `InvalidCharacterError`. Use a mutation method such as `appendData` if you intentionally need `"]]>"` in a CDATASection node's data. + +Thank you, [@thesmartshadow](https://github.com/thesmartshadow), for your contributions + + +## [0.8.11](https://github.com/xmldom/xmldom/compare/0.8.10...0.8.11) + +### Fixed + +- update `ownerDocument` when moving nodes between documents [`#933`](https://github.com/xmldom/xmldom/pull/933) / [`#932`](https://github.com/xmldom/xmldom/issues/932) + +Thank you, [@shunkica](https://github.com/shunkica), for your contributions + ## [0.9.8](https://github.com/xmldom/xmldom/compare/0.9.8...0.9.7) ### Fixed
index.d.ts+18 −2 modified@@ -1191,9 +1191,15 @@ declare module '@xmldom/xmldom' { createAttributeNS(namespace: string | null, qualifiedName: string): Attr; /** - * Returns a CDATASection node whose data is data. + * Returns a new CDATASection node whose data is `data`. * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createCDATASection) + * __This implementation differs from the specification:__ - calling this method on an HTML + * document does not throw `NotSupportedError`. + * + * @throws {DOMException} + * With code `INVALID_CHARACTER_ERR` if `data` contains `"]]>"`. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection + * @see https://dom.spec.whatwg.org/#dom-document-createcdatasection */ createCDATASection(data: string): CDATASection; @@ -1458,6 +1464,16 @@ declare module '@xmldom/xmldom' { } class XMLSerializer { + /** + * Returns the result of serializing `node` to XML. + * + * __This implementation differs from the specification:__ - CDATASection nodes whose data + * contains `]]>` are serialized by splitting the section at each `]]>` occurrence (following + * W3C DOM Level 3 Core `split-cdata-sections` + * default behaviour). A configurable option is not yet implemented. + * + * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring + */ serializeToString(node: Node, nodeFilter?: (node: Node) => boolean): string; } // END ./lib/dom.js
lib/dom.js+26 −1 modified@@ -2210,10 +2210,22 @@ Document.prototype = { return node; }, /** + * Returns a new CDATASection node whose data is `data`. + * + * __This implementation differs from the specification:__ - calling this method on an HTML + * document does not throw `NotSupportedError`. + * * @param {string} data * @returns {CDATASection} + * @throws {DOMException} + * With code `INVALID_CHARACTER_ERR` if `data` contains `"]]>"`. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createCDATASection + * @see https://dom.spec.whatwg.org/#dom-document-createcdatasection */ createCDATASection: function (data) { + if (data.indexOf(']]>') !== -1) { + throw new DOMException(DOMException.INVALID_CHARACTER_ERR, 'data contains "]]>"'); + } var node = new CDATASection(PDC); node.ownerDocument = this; node.childNodes = new NodeList(); @@ -2693,6 +2705,19 @@ function ProcessingInstruction(symbol) { ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE; _extends(ProcessingInstruction, CharacterData); function XMLSerializer() {} +/** + * Returns the result of serializing `node` to XML. + * + * __This implementation differs from the specification:__ - CDATASection nodes whose data + * contains `]]>` are serialized by splitting the section at each `]]>` occurrence (following + * W3C DOM Level 3 Core `split-cdata-sections` + * default behaviour). A configurable option is not yet implemented. + * + * @param {Node} node + * @param {function} [nodeFilter] + * @returns {string} + * @see https://html.spec.whatwg.org/#dom-xmlserializer-serializetostring + */ XMLSerializer.prototype.serializeToString = function (node, nodeFilter) { return nodeSerializeToString.call(node, nodeFilter); }; @@ -2917,7 +2942,7 @@ function serializeToString(node, buf, nodeFilter, visibleNamespaces) { */ return buf.push(node.data.replace(/[<&>]/g, _xmlEncoder)); case CDATA_SECTION_NODE: - return buf.push(g.CDATA_START, node.data, g.CDATA_END); + return buf.push(g.CDATA_START, node.data.replace(/]]>/g, ']]]]><![CDATA[>'), g.CDATA_END); case COMMENT_NODE: return buf.push(g.COMMENT_START, node.data, g.COMMENT_END); case DOCUMENT_TYPE_NODE:
test/dom/cdata-section.test.js+43 −2 modified@@ -1,7 +1,10 @@ 'use strict'; -const { describe, test } = require('@jest/globals'); -const { CDATASection } = require('../../lib/dom'); +const { describe, expect, test } = require('@jest/globals'); +const { CDATASection, DOMImplementation } = require('../../lib/dom'); +const { DOMParser } = require('../../lib'); +const { MIME_TYPE } = require('../../lib/conventions'); +const { expectDOMException } = require('../errors/expectDOMException'); describe('CDATASection.prototype', () => { describe('constructor', () => { @@ -10,3 +13,41 @@ describe('CDATASection.prototype', () => { }); }); }); + +describe('Document.prototype.createCDATASection', () => { + let doc; + beforeEach(() => { + const impl = new DOMImplementation(); + doc = impl.createDocument(null, 'xml'); + }); + + describe('throws InvalidCharacterError', () => { + test('when data is exactly "]]>"', () => { + expectDOMException(() => doc.createCDATASection(']]>'), 'InvalidCharacterError'); + }); + + test('when data starts with safe content then contains "]]>"', () => { + expectDOMException(() => doc.createCDATASection('safe]]>data'), 'InvalidCharacterError'); + }); + + test('when data contains multiple "]]>" occurrences', () => { + expectDOMException(() => doc.createCDATASection('before]]>after]]>after'), 'InvalidCharacterError'); + }); + }); + + describe('does not throw', () => { + test('when data is safe', () => { + expect(() => doc.createCDATASection('safe')).not.toThrow(); + }); + + test('when data is empty string', () => { + expect(() => doc.createCDATASection('')).not.toThrow(); + }); + }); + + describe('parse path', () => { + test('parsing XML containing a CDATA section does not throw', () => { + expect(() => new DOMParser().parseFromString('<root><![CDATA[some data]]></root>', MIME_TYPE.XML_TEXT)).not.toThrow(); + }); + }); +});
test/dom/character-data.test.js+46 −2 modified@@ -1,7 +1,9 @@ 'use strict'; -const { describe, test } = require('@jest/globals'); -const { CharacterData } = require('../../lib/dom'); +const { describe, expect, test } = require('@jest/globals'); +const { CharacterData, DOMImplementation } = require('../../lib/dom'); +const { DOMParser, XMLSerializer } = require('../../lib'); +const { MIME_TYPE } = require('../../lib/conventions'); describe('CharacterData.prototype', () => { describe('constructor', () => { @@ -10,3 +12,45 @@ describe('CharacterData.prototype', () => { }); }); }); + +describe('CDATASection mutation vectors produce safe serializer output', () => { + let doc; + // Serializes, then re-parses, and returns true if an <injected> element appears in the tree. + function isInjected(root) { + const xml = new XMLSerializer().serializeToString(root); + const reparsed = new DOMParser().parseFromString(xml, MIME_TYPE.XML_TEXT); + return reparsed.getElementsByTagName('injected').length > 0; + } + + beforeEach(() => { + doc = new DOMImplementation().createDocument(null, 'root', null); + }); + + test('appendData introduces "]]>" safely', () => { + const cdata = doc.createCDATASection('safe'); + doc.documentElement.appendChild(cdata); + cdata.appendData(']]><injected/>'); + expect(isInjected(doc.documentElement)).toBe(false); + }); + + test('replaceData introduces "]]>" safely', () => { + const cdata = doc.createCDATASection('safe data'); + doc.documentElement.appendChild(cdata); + cdata.replaceData(4, 5, ']]><injected/>'); + expect(isInjected(doc.documentElement)).toBe(false); + }); + + test('.data assignment introduces "]]>" safely', () => { + const cdata = doc.createCDATASection('safe'); + doc.documentElement.appendChild(cdata); + cdata.data = 'evil]]><injected/>'; + expect(isInjected(doc.documentElement)).toBe(false); + }); + + test('.textContent assignment introduces "]]>" safely', () => { + const cdata = doc.createCDATASection('safe'); + doc.documentElement.appendChild(cdata); + cdata.textContent = 'evil]]><injected/>'; + expect(isInjected(doc.documentElement)).toBe(false); + }); +});
test/dom/serializer.test.js+38 −0 modified@@ -2,6 +2,7 @@ const { DOMParser, XMLSerializer } = require('../../lib'); const { MIME_TYPE } = require('../../lib/conventions'); +const { DOMImplementation } = require('../../lib/dom'); describe('XML Serializer', () => { test('supports text node containing "]]>"', () => { @@ -227,3 +228,40 @@ describe('XML Serializer', () => { }); }); }); + +describe('XMLSerializer CDATASection serialization', () => { + let doc; + beforeEach(() => { + doc = new DOMImplementation().createDocument(null, 'root', null); + }); + + test('serializes a safe CDATASection unchanged', () => { + doc.documentElement.appendChild(doc.createCDATASection('safe data')); + expect(new XMLSerializer().serializeToString(doc.documentElement)).toBe('<root><![CDATA[safe data]]></root>'); + }); + + test('splits a CDATASection whose data contains "]]>"', () => { + const cdata = doc.createCDATASection('safe'); + cdata.data = 'foo]]>bar'; + doc.documentElement.appendChild(cdata); + expect(new XMLSerializer().serializeToString(doc.documentElement)).toBe('<root><![CDATA[foo]]]]><![CDATA[>bar]]></root>'); + }); + + test('splits multiple "]]>" occurrences', () => { + const cdata = doc.createCDATASection('safe'); + cdata.data = 'a]]>b]]>c'; + doc.documentElement.appendChild(cdata); + expect(new XMLSerializer().serializeToString(doc.documentElement)).toBe( + '<root><![CDATA[a]]]]><![CDATA[>b]]]]><![CDATA[>c]]></root>' + ); + }); + + test('split output round-trips through DOMParser to equivalent content', () => { + const cdata = doc.createCDATASection('safe'); + cdata.data = 'foo]]>bar'; + doc.documentElement.appendChild(cdata); + const serialized = new XMLSerializer().serializeToString(doc.documentElement); + const reparsed = new DOMParser().parseFromString(serialized, MIME_TYPE.XML_TEXT); + expect(reparsed.documentElement.textContent).toBe('foo]]>bar'); + }); +});
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-wh4c-j3r5-mjhpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34601ghsaADVISORY
- github.com/xmldom/xmldom/commit/2b852e836ab86dbbd6cbaf0537f584dd0b5ac184nvdWEB
- github.com/xmldom/xmldom/releases/tag/0.8.12nvdWEB
- github.com/xmldom/xmldom/releases/tag/0.9.9nvdWEB
- github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhpnvdWEB
News mentions
0No linked articles in our index yet.