GHSA-WH4C-J3R5-MJHP

Vulnerability from github – Published: 2026-04-01 00:19 – Updated: 2026-04-24 23:17
VLAI?
Summary
xmldom: XML injection via unsafe CDATA serialization allows attacker-controlled markup insertion
Details

Summary

@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.

The sequence ]]> is not allowed inside CDATA content and must be rejected or safely handled during serialization. (MDN Web Docs)

Attack surface

Document.createCDATASection(data) is the most direct entry point, but it is not the only one. The WHATWG DOM spec intentionally does not validate ]]> in mutation methods — only createCDATASection carries that guard. The following paths therefore also allow ]]> to enter a CDATASection node and reach the serializer:

  • CharacterData.appendData()
  • CharacterData.replaceData()
  • CharacterData.insertData()
  • Direct assignment to .data
  • Direct assignment to .textContent

(Note: assigning to .nodeValue does not update .data in this implementation — the serializer reads .data directly — so .nodeValue is not an exploitable path.)

Parse path

Parsing XML that contains a CDATA section is not affected. The SAX parser's non-greedy CDSect regex stops at the first ]]>, so parsed CDATA data never contains the terminator.


Impact

If an application uses xmldom to generate "trusted" XML documents that embed untrusted user input inside CDATA (a common pattern in exports, feeds, SOAP/XML integrations, etc.), an attacker can inject additional XML elements/attributes into the generated document.

This can lead to:

  • Integrity violation of generated XML documents.
  • Business-logic injection in downstream consumers (e.g., injecting <approved>true</approved>, <role>admin</role>, workflow flags, or other security-relevant elements).
  • Unexpected privilege/workflow decisions if downstream logic assumes injected nodes cannot appear.

This issue does not require malformed parsers or browser behavior; it is caused by serialization producing attacker-influenced XML markup.


Root Cause (with file + line numbers)

File: lib/dom.js

1. No validation in createCDATASection

createCDATASection: function (data) accepts any string and appends it directly.

  • Lines 2216–2221 (0.9.8)

2. Unsafe CDATA serialization

Serializer prints CDATA sections as:

<![CDATA[ + node.data + ]]>

without handling ]]> in the data.

  • Lines 2919–2920 (0.9.8)

Because CDATA content is emitted verbatim, an embedded ]]> closes the CDATA section early and the remainder of the attacker-controlled payload is interpreted as markup in the serialized XML.


Proof of Concept — Fix A: createCDATASection now throws

On patched versions, passing ]]> directly to createCDATASection throws InvalidCharacterError instead of silently accepting the payload:

const { DOMImplementation } = require('./lib');

const doc = new DOMImplementation().createDocument(null, 'root', null);
try {
  doc.createCDATASection('SAFE]]><injected attr="pwn"/>');
  console.log('VULNERABLE — no error thrown');
} catch (e) {
  console.log('FIXED — threw:', e.name); // InvalidCharacterError
}

Expected output on patched versions:

FIXED — threw: InvalidCharacterError

Proof of Concept — Fix B: mutation vector now safe

On patched versions, injecting ]]> via a mutation method (appendData, replaceData, .data =, .textContent =) no longer produces injectable output. The serializer splits the terminator so the result round-trips as safe text:

const { DOMImplementation, XMLSerializer } = require('./lib');
const { DOMParser } = require('./lib');

const doc = new DOMImplementation().createDocument(null, 'root', null);

// Start with safe data, then mutate to include the terminator
const cdata = doc.createCDATASection('safe');
doc.documentElement.appendChild(cdata);
cdata.appendData(']]><injected attr="pwn"/><more>TEXT</more><![CDATA[');

const out = new XMLSerializer().serializeToString(doc);
console.log('Serialized:', out);

const reparsed = new DOMParser().parseFromString(out, 'text/xml');
const injected = reparsed.getElementsByTagName('injected').length > 0;
console.log('Injected element found in reparsed doc:', injected);
// VULNERABLE: true  |  FIXED: false

Expected output on patched versions:

Serialized: <root><![CDATA[safe]]]]><![CDATA[><injected attr="pwn"/><more>TEXT</more><![CDATA[]]></root>
Injected element found in reparsed doc: false

Fix Applied

Both mitigations were implemented:

Option A — Strict/spec-aligned: reject ]]> in createCDATASection()

Document.createCDATASection(data) now throws InvalidCharacterError (per the WHATWG DOM spec) when data contains ]]>. This closes the direct entry point.

Code that previously passed a string containing ]]> to createCDATASection and relied on the silent/unsafe behaviour will now receive InvalidCharacterError. Use a mutation method such as appendData if you intentionally need ]]> in a CDATASection node's data (the serializer split in Option B will keep the output safe).

Option B — Defensive serialization: split the terminator during serialization

XMLSerializer now replaces every occurrence of ]]> in CDATA section data with the split sequence ]]]]><![CDATA[> before emitting. This closes all mutation-vector paths that Option A alone cannot guard, and means the serialized output is always well-formed XML regardless of how ]]> entered the node.

Update — 2026-04-xx (0.9.10 / 0.8.13)

splitCDATASections is deprecated

The CDATA split behavior introduced as Option B of this fix (replacing ]]> with]]]]><![CDATA[> during serialization) is deprecated as of 0.9.10 / 0.8.13.

This release introduces a requireWellFormed option on XMLSerializer.serializeToString(). When { requireWellFormed: true } is passed as the second argument, the serializer throws InvalidStateError if CDATA section data contains ]]> — this is the spec-aligned behavior (W3C DOM Parsing and Serialization, require well-formed flag) and the recommended migration path going forward. The split behavior is now controlled by an explicit splitCDATASections option (default true, preserving the current behavior). The three serialization behaviors are: | requireWellFormed | splitCDATASections | Behavior ||---|---|---|| false (default) | true (default) | Split ]]>]]]]><![CDATA[> (current behavior, deprecated) || true | — (ignored) | Throw InvalidStateError — spec-aligned, recommended |\ false | false | Emit verbatim — same as pre-0.9.9 behavior |

requireWellFormed: true takes precedence: the split path is unreachable when it is set.

Migration

Replace any reliance on the default split behavior with an explicit opt-in: ```js// Before (implicit split, deprecated): const xml = new XMLSerializer().serializeToString(doc);

// After (explicit guard, spec-aligned): const xml = new XMLSerializer().serializeToString(doc, { requireWellFormed: true }); // Throws InvalidStateError if any CDATASection contains ']]>' ```

Removal timeline

Both the splitCDATASections option and the underlying ]]>]]]]><![CDATA[> split mechanics will be removed in the next breaking (0.10.0) release. After removal, the only behaviors will be verbatim (default) and requireWellFormed: true (throws).

Removal is tracked in xmldom/xmldom#999.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "xmldom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "0.6.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@xmldom/xmldom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.8.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "@xmldom/xmldom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.9.0"
            },
            {
              "fixed": "0.9.9"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-34601"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-91"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-01T00:19:06Z",
    "nvd_published_at": "2026-04-02T18:16:31Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`@xmldom/xmldom` allows attacker-controlled strings containing the CDATA terminator `]]\u003e` 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\ninjection and downstream business-logic manipulation.\n\nThe sequence `]]\u003e` is not allowed inside CDATA content and must be rejected or safely handled during serialization. ([MDN Web Docs](https://developer.mozilla.org/))\n\n### Attack surface\n\n`Document.createCDATASection(data)` is the most direct entry point, but it is not the only one. The WHATWG DOM spec intentionally does not validate `]]\u003e` in mutation methods \u2014 only `createCDATASection` carries that guard. The following paths therefore also allow `]]\u003e` to enter a CDATASection node and reach the serializer:\n\n- `CharacterData.appendData()`\n- `CharacterData.replaceData()`\n- `CharacterData.insertData()`\n- Direct assignment to `.data`\n- Direct assignment to `.textContent`\n\n(Note: assigning to `.nodeValue` does **not** update `.data` in this implementation \u2014 the serializer reads `.data` directly \u2014 so `.nodeValue` is not an exploitable path.)\n\n### Parse path\n\nParsing XML that contains a CDATA section is **not** affected. The SAX parser\u0027s non-greedy `CDSect` regex stops at the first `]]\u003e`, so parsed CDATA data never contains the terminator.\n\n---\n\n## Impact\n\nIf an application uses `xmldom` to generate \"trusted\" XML documents that embed **untrusted user input** inside CDATA (a common pattern in exports, feeds, SOAP/XML integrations, etc.), an attacker can inject additional XML elements/attributes into the generated document.\n\nThis can lead to:\n\n- Integrity violation of generated XML documents.\n- Business-logic injection in downstream consumers (e.g., injecting `\u003capproved\u003etrue\u003c/approved\u003e`,  `\u003crole\u003eadmin\u003c/role\u003e`, workflow flags, or other security-relevant elements).\n- Unexpected privilege/workflow decisions if downstream logic assumes injected nodes cannot appear.\n\nThis issue does **not** require malformed parsers or browser behavior; it is caused by serialization producing attacker-influenced XML markup.\n\n---\n\n## Root Cause (with file + line numbers)\n\n**File:** `lib/dom.js`\n\n### 1. No validation in `createCDATASection`\n\n`createCDATASection: function (data)` accepts any string and appends it directly.\n\n- **Lines 2216\u20132221** (0.9.8)\n\n### 2. Unsafe CDATA serialization\n\nSerializer prints CDATA sections as:\n\n```\n\u003c![CDATA[ + node.data + ]]\u003e\n```\n\nwithout handling `]]\u003e` in the data.\n\n- **Lines 2919\u20132920** (0.9.8)\n\nBecause CDATA content is emitted verbatim, an embedded `]]\u003e` closes the CDATA section early and the remainder of the attacker-controlled payload is interpreted as markup in the serialized XML.\n\n---\n\n## Proof of Concept \u2014 Fix A: `createCDATASection` now throws\n\nOn patched versions, passing `]]\u003e` directly to `createCDATASection` throws `InvalidCharacterError` instead of silently accepting the payload:\n\n```js\nconst { DOMImplementation } = require(\u0027./lib\u0027);\n\nconst doc = new DOMImplementation().createDocument(null, \u0027root\u0027, null);\ntry {\n  doc.createCDATASection(\u0027SAFE]]\u003e\u003cinjected attr=\"pwn\"/\u003e\u0027);\n  console.log(\u0027VULNERABLE \u2014 no error thrown\u0027);\n} catch (e) {\n  console.log(\u0027FIXED \u2014 threw:\u0027, e.name); // InvalidCharacterError\n}\n```\n\nExpected output on patched versions:\n\n```\nFIXED \u2014 threw: InvalidCharacterError\n```\n\n---\n\n## Proof of Concept \u2014 Fix B: mutation vector now safe\n\nOn patched versions, injecting `]]\u003e` via a mutation method (`appendData`, `replaceData`, `.data =`, `.textContent =`) no longer produces injectable output. The serializer splits the terminator so the result round-trips as safe text:\n\n```js\nconst { DOMImplementation, XMLSerializer } = require(\u0027./lib\u0027);\nconst { DOMParser } = require(\u0027./lib\u0027);\n\nconst doc = new DOMImplementation().createDocument(null, \u0027root\u0027, null);\n\n// Start with safe data, then mutate to include the terminator\nconst cdata = doc.createCDATASection(\u0027safe\u0027);\ndoc.documentElement.appendChild(cdata);\ncdata.appendData(\u0027]]\u003e\u003cinjected attr=\"pwn\"/\u003e\u003cmore\u003eTEXT\u003c/more\u003e\u003c![CDATA[\u0027);\n\nconst out = new XMLSerializer().serializeToString(doc);\nconsole.log(\u0027Serialized:\u0027, out);\n\nconst reparsed = new DOMParser().parseFromString(out, \u0027text/xml\u0027);\nconst injected = reparsed.getElementsByTagName(\u0027injected\u0027).length \u003e 0;\nconsole.log(\u0027Injected element found in reparsed doc:\u0027, injected);\n// VULNERABLE: true  |  FIXED: false\n```\n\nExpected output on patched versions:\n\n```\nSerialized: \u003croot\u003e\u003c![CDATA[safe]]]]\u003e\u003c![CDATA[\u003e\u003cinjected attr=\"pwn\"/\u003e\u003cmore\u003eTEXT\u003c/more\u003e\u003c![CDATA[]]\u003e\u003c/root\u003e\nInjected element found in reparsed doc: false\n```\n\n---\n\n## Fix Applied\n\nBoth mitigations were implemented:\n\n### Option A \u2014 Strict/spec-aligned: reject `]]\u003e` in `createCDATASection()`\n\n`Document.createCDATASection(data)` now throws `InvalidCharacterError` (per the [WHATWG DOM spec](https://dom.spec.whatwg.org/#dom-document-createcdatasection)) when `data` contains `]]\u003e`. This closes the direct entry point.\n\nCode that previously passed a string containing `]]\u003e` to `createCDATASection` and relied on the silent/unsafe behaviour will now receive `InvalidCharacterError`. Use a mutation method such as `appendData` if you intentionally need `]]\u003e` in a CDATASection node\u0027s data (the serializer split in Option B will keep the output safe).\n\n### Option B \u2014 Defensive serialization: split the terminator during serialization\n\n`XMLSerializer` now replaces every occurrence of `]]\u003e` in CDATA section data with the split sequence `]]]]\u003e\u003c![CDATA[\u003e` before emitting. This closes all mutation-vector paths that Option A alone cannot guard, and means the serialized output is always well-formed XML regardless of how `]]\u003e` entered the node.\n\n## Update \u2014 2026-04-xx (0.9.10 / 0.8.13)\n\n### `splitCDATASections` is deprecated\n\nThe CDATA split behavior introduced as Option B of this fix (replacing `]]\u003e` with`]]]]\u003e\u003c![CDATA[\u003e` during serialization) is **deprecated** as of 0.9.10 / 0.8.13.\n\nThis release introduces a `requireWellFormed` option on `XMLSerializer.serializeToString()`. When `{ requireWellFormed: true }` is passed as the second argument, the serializer throws `InvalidStateError` if CDATA section data contains `]]\u003e` \u2014 this is the spec-aligned behavior (W3C DOM Parsing and Serialization, `require well-formed` flag) and the recommended migration path going forward.\nThe split behavior is now controlled by an explicit `splitCDATASections` option (default `true`, preserving the current behavior). The three serialization behaviors are:\n| `requireWellFormed` | `splitCDATASections` | Behavior ||---|---|---|| `false` (default) | `true` (default) | Split `]]\u003e` \u2192 `]]]]\u003e\u003c![CDATA[\u003e` (current behavior, deprecated) || `true` | \u2014 (ignored) | Throw `InvalidStateError` \u2014 spec-aligned, recommended |\\ `false` | `false` | Emit verbatim \u2014 same as pre-0.9.9 behavior |\n\n`requireWellFormed: true` takes precedence: the split path is unreachable when it is set.\n\n### Migration\nReplace any reliance on the default split behavior with an explicit opt-in:\n```js// Before (implicit split, deprecated): const xml = new XMLSerializer().serializeToString(doc);\n\n// After (explicit guard, spec-aligned): const xml = new XMLSerializer().serializeToString(doc, { requireWellFormed: true }); // Throws InvalidStateError if any CDATASection contains \u0027]]\u003e\u0027 ```\n\n### Removal timeline\nBoth the `splitCDATASections` option and the underlying `]]\u003e` \u2192 `]]]]\u003e\u003c![CDATA[\u003e` split mechanics will be removed in the next breaking (`0.10.0`) release. After removal, the only behaviors will be verbatim (default) and `requireWellFormed: true` (throws).\n\nRemoval is tracked in [xmldom/xmldom#999](https://github.com/xmldom/xmldom/issues/999).",
  "id": "GHSA-wh4c-j3r5-mjhp",
  "modified": "2026-04-24T23:17:44Z",
  "published": "2026-04-01T00:19:06Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/security/advisories/GHSA-wh4c-j3r5-mjhp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34601"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/commit/2b852e836ab86dbbd6cbaf0537f584dd0b5ac184"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/xmldom/xmldom"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/releases/tag/0.8.12"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/releases/tag/0.9.9"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "xmldom: XML injection via unsafe CDATA serialization allows attacker-controlled markup insertion"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…