GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-W2RR-34G9-RVRJ

Vulnerability from github – Published: 2026-09-08 20:31 – Updated: 2026-09-08 20:31
VLAI
Summary
xmldom: Element name injection via createElement() bypasses requireWellFormed
Details

Summary

Document.createElement() in @xmldom/xmldom accepts arbitrary strings as the tagName parameter with zero validation. The serializer emits the tag name verbatim into XML/HTML output. Critically, the requireWellFormed: true serializer option — the recommended mitigation from CVE-2026-41672, CVE-2026-41674, and CVE-2026-34601 — did NOT catch this, making it a bypass of the existing security controls.

An attacker who controls the element name string can inject arbitrary attributes (including event handlers) into the serialized output, leading to XSS when the output is consumed by a browser or downstream parser.

Details

Document.createElement() accepts any string as tagName and stores it directly on the element node without validation. When the document is later serialized via XMLSerializer.serializeToString(), the serializer emits the tagName verbatim into the output.

The XML specification requires element names to conform to the Name production. The existing createAttributeNS() and createElementNS() methods validate qualified names against an anchored name/QName pattern, but createElement() bypasses this entirely, and the requireWellFormed: true serializer path performed no element-name validation — rendering it ineffective against this vector.

Root Cause

  1. createElement() stores the raw tagName string without any validation.
  2. The serializer's requireWellFormed code path did not validate element names against the XML Name/QName production.
  3. The serializer emits tagName directly into angle brackets: <${tagName}...>.

Proof of Concept

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

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

// Inject an element whose "name" contains attributes with an XSS payload
const el = doc.createElement('img src=x onerror="alert(1)"');
doc.documentElement.appendChild(el);

const output = serializer.serializeToString(doc, { requireWellFormed: true });
console.log(output);
// <root><img src=x onerror="alert(1)"/></root>
//
// A browser parsing this HTML will execute alert(1).
// requireWellFormed: true did NOT prevent the injection.

Impact

Applications that use @xmldom/xmldom to construct DOM trees and serialize them to XML/HTML are vulnerable to injection attacks if any part of an element name originates from user input. This includes:

  • Cross-Site Scripting (XSS): Injecting event handler attributes (onerror, onclick, etc.) into HTML output consumed by browsers.
  • XML injection: Breaking XML document structure by injecting closing tags, new elements, or processing instructions through the element name.
  • Security control bypass: Applications that adopted requireWellFormed: true as a mitigation for CVE-2026-41672 / 41674 / 34601 remained vulnerable through this vector.

@xmldom/xmldom can also be used inside browsers, where it mirrors the DOM API. Unlike the browser's createElement(), which rejects an invalid name with InvalidCharacterError, xmldom accepts it — developers may assume the same safety and skip validation.

Fix Applied

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

When { requireWellFormed: true } is passed, the serializer now validates each element's serialized qualified name against the XML QName production and throws InvalidStateError before emitting the start tag. This also covers the namespace-prefix sub-vector: an invalid prefix surfaces either in the element qualified name (PREFIX:local) or in a synthesized xmlns:PREFIX declaration, and both are QName-checked.

Fixed under requireWellFormed: true in @xmldom/xmldom 0.9.11 and 0.8.14. Default serialization is unchanged.

PoC — fixed path

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

const doc = new DOMImplementation().createDocument(null, 'root', null);
doc.documentElement.appendChild(doc.createElement('img src=x onerror="alert(1)"'));

// Default (unchanged): verbatim — injection present
console.log(new XMLSerializer().serializeToString(doc));
// <root><img src=x onerror="alert(1)"/></root>

// Opt-in guard: throws InvalidStateError before serializing
try {
  new XMLSerializer().serializeToString(doc, { requireWellFormed: true });
} catch (e) {
  console.log(e.name, e.message);
  // InvalidStateError: The element name "img src=x onerror="alert(1)"" is not a valid XML QName
}

Why the default stays verbatim

The W3C DOM Parsing and Serialization spec defines a require well-formed flag whose default value is false. With the flag unset, the serializer emits element names verbatim, matching the XMLSerializer behavior of Chrome, Firefox, and Safari. Unconditionally throwing would be a behavioral breaking change with no spec justification; the opt-in requireWellFormed: true flag lets applications that require injection safety enable strict mode without breaking existing code.

Residual limitation

createElement(tagName) does not validate tagName at creation time. Enforcing an InvalidCharacterError for invalid names unconditionally at creation time is a breaking change and is deferred to the next breaking release. When the default serialization path is used (without requireWellFormed: true), invalid element names are still emitted verbatim; applications that do not pass requireWellFormed: true remain exposed.

Creation-time validation is tracked in a public issue on the next breaking-release milestone (filed at publication — issue link to be added), targeting the next breaking release.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.9.10"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@xmldom/xmldom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.9.0"
            },
            {
              "fixed": "0.9.11"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.8.13"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@xmldom/xmldom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.7.0"
            },
            {
              "fixed": "0.8.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "xmldom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "0.6.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-83607"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-91"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-08T20:31:08Z",
    "nvd_published_at": "2026-09-01T15:17:38Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`Document.createElement()` in `@xmldom/xmldom` accepts arbitrary strings as the `tagName` parameter with zero validation. The serializer emits the tag name verbatim into XML/HTML output. Critically, the `requireWellFormed: true` serializer option \u2014 the recommended mitigation from CVE-2026-41672, CVE-2026-41674, and CVE-2026-34601 \u2014 did NOT catch this, making it a bypass of the existing security controls.\n\nAn attacker who controls the element name string can inject arbitrary attributes (including event handlers) into the serialized output, leading to XSS when the output is consumed by a browser or downstream parser.\n\n## Details\n\n`Document.createElement()` accepts any string as `tagName` and stores it directly on the element node without validation. When the document is later serialized via `XMLSerializer.serializeToString()`, the serializer emits the `tagName` verbatim into the output.\n\nThe XML specification requires element names to conform to the `Name` production. The existing `createAttributeNS()` and `createElementNS()` methods validate qualified names against an anchored name/`QName` pattern, but `createElement()` bypasses this entirely, and the `requireWellFormed: true` serializer path performed no element-name validation \u2014 rendering it ineffective against this vector.\n\n### Root Cause\n\n1. `createElement()` stores the raw `tagName` string without any validation.\n2. The serializer\u0027s `requireWellFormed` code path did not validate element names against the XML `Name`/`QName` production.\n3. The serializer emits `tagName` directly into angle brackets: `\u003c${tagName}...\u003e`.\n\n## Proof of Concept\n\n```js\nconst { DOMImplementation, XMLSerializer } = require(\u0027@xmldom/xmldom\u0027);\n\nconst impl = new DOMImplementation();\nconst serializer = new XMLSerializer();\nconst doc = impl.createDocument(null, \u0027root\u0027, null);\n\n// Inject an element whose \"name\" contains attributes with an XSS payload\nconst el = doc.createElement(\u0027img src=x onerror=\"alert(1)\"\u0027);\ndoc.documentElement.appendChild(el);\n\nconst output = serializer.serializeToString(doc, { requireWellFormed: true });\nconsole.log(output);\n// \u003croot\u003e\u003cimg src=x onerror=\"alert(1)\"/\u003e\u003c/root\u003e\n//\n// A browser parsing this HTML will execute alert(1).\n// requireWellFormed: true did NOT prevent the injection.\n```\n\n## Impact\n\nApplications that use `@xmldom/xmldom` to construct DOM trees and serialize them to XML/HTML are vulnerable to injection attacks if any part of an element name originates from user input. This includes:\n\n- **Cross-Site Scripting (XSS)**: Injecting event handler attributes (`onerror`, `onclick`, etc.) into HTML output consumed by browsers.\n- **XML injection**: Breaking XML document structure by injecting closing tags, new elements, or processing instructions through the element name.\n- **Security control bypass**: Applications that adopted `requireWellFormed: true` as a mitigation for CVE-2026-41672 / 41674 / 34601 remained vulnerable through this vector.\n\n`@xmldom/xmldom` can also be used inside browsers, where it mirrors the DOM API. Unlike the browser\u0027s `createElement()`, which rejects an invalid name with `InvalidCharacterError`, xmldom accepts it \u2014 developers may assume the same safety and skip validation.\n\n## Fix Applied\n\n\u003e **\u26a0 Opt-in required.** Protection is not automatic. Existing serialization calls remain\n\u003e vulnerable unless `{ requireWellFormed: true }` is explicitly passed. Applications that\n\u003e serialize untrusted DOM content should audit all `serializeToString()` call sites and add it.\n\nWhen `{ requireWellFormed: true }` is passed, the serializer now validates each element\u0027s serialized qualified name against the XML `QName` production and throws `InvalidStateError` before emitting the start tag. This also covers the **namespace-prefix** sub-vector: an invalid prefix surfaces either in the element qualified name (`PREFIX:local`) or in a synthesized `xmlns:PREFIX` declaration, and both are QName-checked.\n\nFixed under `requireWellFormed: true` in `@xmldom/xmldom` **0.9.11** and **0.8.14**. Default serialization is unchanged.\n\n### PoC \u2014 fixed path\n\n```js\nconst { DOMImplementation, XMLSerializer } = require(\u0027@xmldom/xmldom\u0027);\n\nconst doc = new DOMImplementation().createDocument(null, \u0027root\u0027, null);\ndoc.documentElement.appendChild(doc.createElement(\u0027img src=x onerror=\"alert(1)\"\u0027));\n\n// Default (unchanged): verbatim \u2014 injection present\nconsole.log(new XMLSerializer().serializeToString(doc));\n// \u003croot\u003e\u003cimg src=x onerror=\"alert(1)\"/\u003e\u003c/root\u003e\n\n// Opt-in guard: throws InvalidStateError before serializing\ntry {\n  new XMLSerializer().serializeToString(doc, { requireWellFormed: true });\n} catch (e) {\n  console.log(e.name, e.message);\n  // InvalidStateError: The element name \"img src=x onerror=\"alert(1)\"\" is not a valid XML QName\n}\n```\n\n### Why the default stays verbatim\n\nThe W3C DOM Parsing and Serialization spec defines a `require well-formed` flag whose **default value is `false`**. With the flag unset, the serializer emits element names verbatim, matching the `XMLSerializer` behavior of Chrome, Firefox, and Safari. Unconditionally throwing would be a behavioral breaking change with no spec justification; the opt-in `requireWellFormed: true` flag lets applications that require injection safety enable strict mode without breaking existing code.\n\n### Residual limitation\n\n`createElement(tagName)` does not validate `tagName` at creation time. Enforcing an `InvalidCharacterError` for invalid names unconditionally at creation time is a breaking change and is deferred to the next breaking release. When the default serialization path is used (without `requireWellFormed: true`), invalid element names are still emitted verbatim; applications that do not pass `requireWellFormed: true` remain exposed.\n\nCreation-time validation is tracked in a public issue on the next breaking-release milestone (filed at publication \u2014 issue link to be added), targeting the next breaking release.",
  "id": "GHSA-w2rr-34g9-rvrj",
  "modified": "2026-09-08T20:31:08Z",
  "published": "2026-09-08T20:31:08Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/security/advisories/GHSA-w2rr-34g9-rvrj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-83607"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/pull/1043"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/pull/1050"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/commit/cba1321218b069182695813fa7565653708e172e"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/commit/d8212e632507eaf1d9f609657dd4c56abeb12d44"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/xmldom/xmldom"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/releases/tag/0.8.14"
    },
    {
      "type": "WEB",
      "url": "https://github.com/xmldom/xmldom/releases/tag/0.9.11"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "xmldom: Element name injection via createElement() bypasses requireWellFormed"
}



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…

Loading…