CVE-2026-83605: @xmldom/xmldom Attribute Name Injection via setAttribute()
A flaw in @xmldom/xmldom lets an attacker smuggle extra HTML attributes, including event handlers like onclick, into serialized XML output by passing a crafted string as an attribute name to…

The problem
Element.setAttribute() in @xmldom/xmldom routes through the private _createAttribute() method, which performs no name validation. The public createAttribute() correctly rejects names that violate the XML QName production, but setAttribute() never calls it.
The serializer escapes attribute values but emits attribute names verbatim. An attacker who controls any portion of the name string can terminate the current attribute and inject additional ones, including event handlers such as onclick. Passing requireWellFormed: true to serializeToString() did not help, because the serializer never validated names in that code path either.
Proof of concept
A working proof-of-concept for CVE-2026-83605 in @xmldom/xmldom, with the exact payload below.
const { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');
const doc = new DOMImplementation().createDocument(null, 'root', null);
// The attribute name itself carries the injection.
// A closing quote + space + new attribute name is embedded in the name string.
doc.documentElement.setAttribute('class="safe" onclick', 'alert(1)');
// Default serialization: injection present, no error.
console.log(new XMLSerializer().serializeToString(doc));
// => <root class="safe" onclick="alert(1)"/>
//
// One setAttribute() call produced TWO attributes:
// class="safe" and onclick="alert(1)"
// requireWellFormed: true also failed to block it (pre-patch).
console.log(
new XMLSerializer().serializeToString(doc, { requireWellFormed: true })
);
// => <root class="safe" onclick="alert(1)"/> (no error thrown)The root cause is a two-tier validation system. setAttribute() calls the private _createAttribute() path (no validation), while createAttribute() validates against the anchored XML QName production and throws INVALID_CHARACTER_ERR for the same input. Because the serializer emits attribute names verbatim, embedding a literal quote and space in the name string breaks out of the current attribute context and starts a new one.
The patch (commits cba1321 and d8212e6, PRs #1043 and #1050) extends the requireWellFormed serialization path to validate each attribute's qualified name against the QName regex before emitting it, throwing InvalidStateError on a mismatch. This also covers synthesized xmlns:PREFIX namespace declarations.
The default serialization path (requireWellFormed omitted or false) is deliberately unchanged to match the W3C spec, which defines the require well-formed flag as false by default.
The fix
Upgrade to @xmldom/xmldom 0.9.11 (or 0.8.14 on the 0.8.x line). After upgrading, pass { requireWellFormed: true } to every serializeToString() call that processes untrusted DOM content. Without that flag the serializer still emits attribute names verbatim by spec design, so the opt-in is mandatory.
Creation-time validation inside setAttribute() itself is deferred to the next breaking release; until then, requireWellFormed: true is the only runtime guard. The legacy xmldom package (<=0.6.0) has no fixed version.
Related research
- highCVE-2026-83607CVE-2026-83607: @xmldom/xmldom Element Name Injection via createElement()
- highCVE-2026-83616CVE-2026-83616: @xmldom/xmldom Processing Instruction Target Injection
- highCVE-2026-83618CVE-2026-83618: @xmldom/xmldom requireWellFormed DocType Injection Bypass via Embedded Line Terminator
- highCVE-2026-83609CVE-2026-83609: @xmldom/xmldom XML Injection via Line Terminator in QName Validation