# xmldom: Attribute name injection via setAttribute() bypasses requireWellFormed

xmldom's setAttribute() skips QName validation, letting attacker-controlled attribute names inject extra attributes like onclick into serialized XML, enabling XSS.

- Payout: Research
- Severity: high
- Program: xmldom
- Found by: bhaswanthc
- Disclosed: 2026-09-08
- CVE: CVE-2026-83605
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4w3w-2rp5-g8jm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4w3w-2rp5-g8jm
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:xss

**Target:** npm XML DOM library (Element.setAttribute / XMLSerializer)
**Root cause:** Element.setAttribute() calls the private _createAttribute() which performs no QName validation, unlike public createAttribute(); the serializer then emits attribute names verbatim, trusting them while escaping only values.
**Steps:**
1. Create doc via `new DOMImplementation().createDocument(null, 'root', null)`
2. Call `doc.documentElement.setAttribute('class="safe" onclick', 'alert(1)')` — no error thrown
3. Serialize with `new XMLSerializer().serializeToString(doc, { requireWellFormed: true })`
4. Output is `<root class="safe" onclick="alert(1)"/>` — one call produced two attributes
5. Contrast: `doc.createAttribute('class="safe" onclick')` throws INVALID_CHARACTER_ERR
**How found:** Noticed a two-tier validation asymmetry: public createAttribute() validates against QName but the far more common setAttribute() routes through unvalidated private _createAttribute(); tested quote/space characters in the name to break out of the attribute and inject new ones, and confirmed requireWellFormed did not catch it.
**Exploitation:** Any app passing user-controlled data into the attribute-name argument of setAttribute() gets attacker-chosen attributes (event handlers, integrity/nonce/sandbox overrides) in serialized output consumed by browsers, yielding XSS or security-attribute override.
**Insight:** When a library exposes both a validating public API and a private unvalidated helper, check which one the convenience methods actually call — the popular path is often the unvalidated one, and serializers that escape values may still trust names.

