{"id":"CVE-2026-83617","aliases":["GHSA-jxjr-3g7g-3944"],"url":"https://o3.security/vulnerability/CVE-2026-83617","summary":"xmldom: requireWellFormed element/attribute name validation is bypassable via an embedded line terminator","details":"## Summary\n\nAn embedded line terminator bypasses the `requireWellFormed` serializer check for element and\nattribute names. The check was added to fix GHSA-w2rr-34g9-rvrj and GHSA-4w3w-2rp5-g8jm; a name whose\nfirst line is well-formed slips past it and is serialized verbatim, so the characters after the line\nterminator break out of the start/end tag or attribute. Callers who enabled `requireWellFormed`\nspecifically to neutralize those name-injection issues remain exposed.\n\n## Details\n\nxmldom builds every grammar production through a shared regexp builder that compiles with the `m`\nflag. The anchored full-string matcher used for element and attribute names, `QName_exact =\nreg('^', QName, '$')`, therefore inherits `m`. When it is applied as `QName_exact.test(name)` against\nan already-assembled node name, the `m` flag makes `$` match at an interior line terminator, so the\nmatcher accepts any value in which **at least one line** is a valid `QName`; the other lines are never\nconstrained. A payload whose first line is a valid `QName`, followed by a line terminator and breakout\nmarkup, is what yields a working injection.\n\nThe serializer emits the accepted name verbatim into element start/end tags and attribute names, so\nthe bytes after the line terminator break out of the intended syntactic position. The check is\nreached whenever a caller serializes, with `requireWellFormed: true`, a node whose name was set\nthrough programmatic DOM construction (`createElement`, `createElementNS`, `createAttribute`,\n`createAttributeNS`) with attacker-influenced input.\n\n### Root Cause\n\n1. A shared regexp builder compiles anchored productions with the `m` flag.\n2. `^…$` under `m` are line anchors, not string anchors.\n3. A full-string validator built on such a production (`.test()`) accepts any string with one\n   conforming line, so a line terminator followed by breakout markup passes.\n\nThe triggering line terminators are the ECMAScript `LineTerminator` set: U+000A, U+000D, U+2028, U+2029.\n\n## Proof of Concept\n\n```js\nconst { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');\n\n// Element name carrying an embedded line terminator + breakout markup:\nconst doc = new DOMImplementation().createDocument(null, 'root', null);\nconst el = doc.createElement('a\\n><script>alert(1)</script');\ndoc.documentElement.appendChild(el);\n\n// Caller opted into well-formed serialization, expecting invalid names to be rejected:\nconsole.log(new XMLSerializer().serializeToString(doc, { requireWellFormed: true }));\n// Observed on the affected version: NO throw; the output contains the injected `><script>…`\n// breakout, because the name's first line (\"a\") satisfies the m-anchored QName check.\n// Expected: InvalidStateError (the name is not a valid XML QName).\n\n// Control — a single-line invalid name IS correctly rejected, proving the check is active and\n// that only the line terminator defeats it:\nconst ctrl = new DOMImplementation().createDocument(null, 'root', null);\nctrl.documentElement.appendChild(ctrl.createElement('a b'));\nnew XMLSerializer().serializeToString(ctrl, { requireWellFormed: true });\n// => throws InvalidStateError: The element name \"a b\" is not a valid XML QName\n```\n\n## Impact\n\n- **Bypass of a previously shipped security mitigation.** Applications that adopted\n  `requireWellFormed: true` specifically to neutralize GHSA-w2rr-34g9-rvrj / GHSA-4w3w-2rp5-g8jm\n  remain exposed to element/attribute name injection.\n- **XML / markup structure injection**, and, where the serialized output is placed into an HTML\n  context, downstream XSS.\n\n## Fix Applied\n\nThe anchored XML `Name`/`QName` validators used by the `requireWellFormed` serializer no\nlonger treat interior line terminators as satisfying the anchors, so a name is validated against the\nwhole string. A name containing a line terminator is rejected with `InvalidStateError`, closing the\nbypass for element and attribute names. The default serialization path is unchanged.\n\n> **⚠ Opt-in required.** Protection is not automatic. Existing serialization calls remain\n> vulnerable unless `{ requireWellFormed: true }` is explicitly passed. Applications that\n> serialize untrusted DOM content should audit all `serializeToString()` call sites and add it.\n\n### Proof of Concept - fixed path\n\n```js\nconst { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');\nconst doc = new DOMImplementation().createDocument(null, 'root', null);\nconst el = doc.createElement('a\\n><script>alert(1)</script');\ndoc.documentElement.appendChild(el);\n\n// Default path (require-well-formed off) — unchanged, still emits the name verbatim,\n// so the `><script>…` bytes break out of the start tag:\nnew XMLSerializer().serializeToString(doc);\n\n// Opted-in path — now rejected:\nnew XMLSerializer().serializeToString(doc, { requireWellFormed: true });\n// throws InvalidStateError: The element name \"a\\n><script>alert(1)</script\" is not a valid XML QName\n```\n\n### Why the default stays verbatim\n\nThe W3C DOM Parsing require-well-formed flag defaults to false, and browser `XMLSerializer` emits\nnames verbatim when it is unset. Throwing unconditionally would be an unjustified breaking change, so\nthe check stays gated on the caller opting in with `{ requireWellFormed: true }`.\n\n### Residual limitation\n\nThe default serialization path (no `requireWellFormed`) still emits names verbatim by design (above).\nNames introduced through `createElement` / `setAttribute` are never validated at creation — those APIs\nstore the name unchecked by design — so the opt-in serializer check remains the only guard on that\npath.","published":"2026-09-01T14:47:46.526Z","modified":"2026-10-15T02:47:33.434810070Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"@xmldom/xmldom","fixedVersion":"0.9.12"}],"fix":{"url":"https://github.com/xmldom/xmldom/commit/7b2ec67e1750daadd0bb06c92e875e726544a362","label":"xmldom/xmldom@7b2ec67"},"references":[{"type":"WEB","url":"https://github.com/xmldom/xmldom/releases/tag/0.9.12"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/83xxx/CVE-2026-83617.json"},{"type":"ADVISORY","url":"https://github.com/xmldom/xmldom/security/advisories/GHSA-jxjr-3g7g-3944"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-83617"},{"type":"FIX","url":"https://github.com/xmldom/xmldom/commit/7b2ec67e1750daadd0bb06c92e875e726544a362"},{"type":"FIX","url":"https://github.com/xmldom/xmldom/pull/1071"},{"type":"PACKAGE","url":"https://github.com/xmldom/xmldom"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-15T02:47:33.434810070Z"}}