{"id":"GHSA-c7q8-3ch8-vqpv","summary":"xmldom: Processing Instruction Target Injection Bypasses requireWellFormed","details":"## Summary\n\n`Document.createProcessingInstruction()` in `@xmldom/xmldom` performs no validation on the `target` parameter. The `requireWellFormed: true` serializer option validates only for `:` in the target and a case-insensitive `xml` prefix, but does not check for `\u003e` characters. A `\u003e` in the target breaks the processing instruction boundary (`\u003c?...?\u003e`), allowing injection of arbitrary content into the serialized XML output.\n\n## Details\n\n`Document.createProcessingInstruction(target, data)` at `lib/dom.js` around line 2413 accepts any string as the `target` parameter and stores it on the PI node without validation.\n\nDuring serialization, the `requireWellFormed` code path (around line 3286) performs two checks on PI targets:\n\n1. Rejects targets containing `:` (namespace prefix check)\n2. Rejects targets matching `xml` case-insensitively (reserved prefix)\n\nHowever, it does NOT validate that the target conforms to the XML Name production, and critically does NOT check for `\u003e` characters. Since processing instructions are serialized as `\u003c?target data?\u003e`, a `\u003e` in the target prematurely closes the PI, causing the remaining content to be interpreted as document content by any downstream XML parser.\n\n### Root Cause\n\n1. `createProcessingInstruction()` performs no validation on `target`\n2. The serializer's `requireWellFormed` check is incomplete -- it only checks for `:` and `xml`, missing characters that break PI syntax (`\u003e`, `?`, whitespace)\n3. The serializer emits the target verbatim: `\u003c?${target} ${data}?\u003e`\n\n## Proof of Concept\n\n```javascript\nconst { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');\n\nconst impl = new DOMImplementation();\nconst serializer = new XMLSerializer();\nconst doc = impl.createDocument(null, 'root', null);\n\n// PI target containing \u003e breaks the PI boundary\nconst pi = doc.createProcessingInstruction('a\u003e', 'data');\ndoc.documentElement.appendChild(pi);\n\nconst output = serializer.serializeToString(doc, { requireWellFormed: true });\nconsole.log(output);\n// Output: \u003croot\u003e\u003c?a\u003e data?\u003e\u003c/root\u003e\n//\n// The \u003e in the target closes the PI prematurely.\n// A downstream XML parser sees:\n//   - Processing instruction: \u003c?a?\u003e  (target \"a\", no data)\n//   - Text content: \" data?\u003e\"\n//\n// requireWellFormed: true did NOT prevent the injection.\n```\n\n### Injecting elements via PI target\n\n```javascript\nconst pi2 = doc.createProcessingInstruction(\n  'a?\u003e\u003cscript xmlns=\"http://www.w3.org/1999/xhtml\"\u003ealert(1)\u003c/script\u003e\u003c?b',\n  ''\n);\ndoc.documentElement.appendChild(pi2);\n\nconst output2 = serializer.serializeToString(doc, { requireWellFormed: true });\nconsole.log(output2);\n// Output includes:\n//   \u003c?a?\u003e\u003cscript xmlns=\"http://www.w3.org/1999/xhtml\"\u003ealert(1)\u003c/script\u003e\u003c?b ?\u003e\n//\n// The injected \u003cscript\u003e element is valid XHTML that a browser would execute.\n```\n\n## Impact\n\nApplications that create processing instructions with user-controlled target strings and serialize the result are vulnerable to XML injection. This enables:\n\n- **XML structure injection**: Breaking the PI boundary to inject arbitrary elements, text, or additional processing instructions into the output\n- **XSS via XHTML**: If the serialized output is served as XHTML or processed by a browser-based XML parser, injected script elements will execute\n- **XXE chain**: Injected DOCTYPE declarations or entity references could trigger XXE in downstream XML parsers that consume the output\n- **requireWellFormed bypass**: The existing well-formedness checks are incomplete and provide a false sense of security\n\n## Fix Applied\n\nUnder `requireWellFormed`, the serializer validates a processing-instruction target as an XML `NCName` (a `Name` with no colon) and rejects a case-insensitive `xml`, throwing `InvalidStateError` when the target is ill-formed — so a `\u003e`, `?`, or whitespace in the target is now refused.\\\nOn 0.9.12 this replaces an earlier check that already rejected a colon or `xml`, so the no-colon rule is preserved.\\\n0.8.15 had no processing-instruction target check at all, so the whole target validation is new there.\\\nNon-breaking and opt-in. See the [XML `Name` production](https://www.w3.org/TR/xml/#NT-Name).\n\u003e **⚠ 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\n### Proof of Concept - fixed path\n\n```javascript\nconst { DOMImplementation, XMLSerializer } = require('@xmldom/xmldom');\n\nconst impl = new DOMImplementation();\nconst serializer = new XMLSerializer();\nconst doc = impl.createDocument(null, 'root', null);\n\n// PI target containing \u003e breaks the PI boundary\nconst pi = doc.createProcessingInstruction('a\u003e', 'data');\ndoc.documentElement.appendChild(pi);\n\n// Default path: emits the ill-formed target verbatim.\nconsole.log(serializer.serializeToString(doc));\n// Output: \u003croot\u003e\u003c?a\u003e data?\u003e\u003c/root\u003e\n\n// Opt-in path: the target check now rejects the break-out character.\ntry {\n  serializer.serializeToString(doc, { requireWellFormed: true });\n} catch (e) {\n  console.log(e.name); // InvalidStateError\n}\n```\n\n### Why the default stays verbatim\n\nW3C DOM Parsing's require-well-formed flag defaults to false, and the browser `XMLSerializer` emits the target verbatim in that default mode. Unconditionally throwing on an ill-formed PI target would diverge from that platform behavior and would be an unjustified breaking change, so the stricter validation is gated behind `{ requireWellFormed: true }`. (See the [W3C XML Name production](https://www.w3.org/TR/xml/#NT-Name) and [XML Processing Instructions](https://www.w3.org/TR/xml/#sec-pi).)\n\n### Residual limitation\n\nThe default serialization path still emits the ill-formed target verbatim -- only the opt-in `requireWellFormed` path is protected. Creation-time validation of the `target` in `createProcessingInstruction()` is breaking and is deferred to the next breaking release, tracked at [xmldom/xmldom#1073](https://github.com/xmldom/xmldom/issues/1073).","aliases":["CVE-2026-83616"],"modified":"2026-09-08T21:15:05.057015449Z","published":"2026-09-08T21:03:28Z","database_specific":{"github_reviewed_at":"2026-09-08T21:03:28Z","nvd_published_at":"2026-09-01T15:17:40Z","cwe_ids":["CWE-91"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/xmldom/xmldom/security/advisories/GHSA-c7q8-3ch8-vqpv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-83616"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/pull/1071"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/pull/1072"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/commit/1cde3e31a07c41c87cfd368d6946aa477f16b4f9"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/commit/3b694872bcb5c7e3cbadba961a4be2488750ce5b"},{"type":"PACKAGE","url":"https://github.com/xmldom/xmldom"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/releases/tag/0.8.15"},{"type":"WEB","url":"https://github.com/xmldom/xmldom/releases/tag/0.9.12"}],"affected":[{"package":{"name":"@xmldom/xmldom","ecosystem":"npm","purl":"pkg:npm/%40xmldom/xmldom"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.7.0"},{"fixed":"0.8.15"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-c7q8-3ch8-vqpv/GHSA-c7q8-3ch8-vqpv.json","last_known_affected_version_range":"\u003c= 0.8.14"}},{"package":{"name":"@xmldom/xmldom","ecosystem":"npm","purl":"pkg:npm/%40xmldom/xmldom"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.9.0"},{"fixed":"0.9.12"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 0.9.11","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-c7q8-3ch8-vqpv/GHSA-c7q8-3ch8-vqpv.json"}},{"package":{"name":"xmldom","ecosystem":"npm","purl":"pkg:npm/xmldom"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"0.6.0"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-c7q8-3ch8-vqpv/GHSA-c7q8-3ch8-vqpv.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","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"}]}