VDB
Sign up
HIGH

GHSA-c7q8-3ch8-vqpv

xmldom: Processing Instruction Target Injection Bypasses requireWellFormed

Quick fix

GHSA-c7q8-3ch8-vqpv — @xmldom/xmldom: upgrade to the fixed version with the command below.

npm install @xmldom/xmldom@0.8.15

Details

## Summary

`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 `>` characters. A `>` in the target breaks the processing instruction boundary (`<?...?>`), allowing injection of arbitrary content into the serialized XML output.

## Details

`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.

During serialization, the `requireWellFormed` code path (around line 3286) performs two checks on PI targets:

1. Rejects targets containing `:` (namespace prefix check) 2. Rejects targets matching `xml` case-insensitively (reserved prefix)

However, it does NOT validate that the target conforms to the XML Name production, and critically does NOT check for `>` characters. Since processing instructions are serialized as `<?target data?>`, a `>` in the target prematurely closes the PI, causing the remaining content to be interpreted as document content by any downstream XML parser.

### Root Cause

1. `createProcessingInstruction()` performs no validation on `target` 2. The serializer's `requireWellFormed` check is incomplete -- it only checks for `:` and `xml`, missing characters that break PI syntax (`>`, `?`, whitespace) 3. The serializer emits the target verbatim: `<?${target} ${data}?>`

## Proof of Concept

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

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

// PI target containing > breaks the PI boundary const pi = doc.createProcessingInstruction('a>', 'data'); doc.documentElement.appendChild(pi);

const output = serializer.serializeToString(doc, { requireWellFormed: true }); console.log(output); // Output: <root><?a> data?></root> // // The > in the target closes the PI prematurely. // A downstream XML parser sees: // - Processing instruction: <?a?> (target "a", no data) // - Text content: " data?>" // // requireWellFormed: true did NOT prevent the injection. ```

### Injecting elements via PI target

```javascript const pi2 = doc.createProcessingInstruction( 'a?><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><?b', '' ); doc.documentElement.appendChild(pi2);

const output2 = serializer.serializeToString(doc, { requireWellFormed: true }); console.log(output2); // Output includes: // <?a?><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script><?b ?> // // The injected <script> element is valid XHTML that a browser would execute. ```

## Impact

Applications that create processing instructions with user-controlled target strings and serialize the result are vulnerable to XML injection. This enables:

- **XML structure injection**: Breaking the PI boundary to inject arbitrary elements, text, or additional processing instructions into the output - **XSS via XHTML**: If the serialized output is served as XHTML or processed by a browser-based XML parser, injected script elements will execute - **XXE chain**: Injected DOCTYPE declarations or entity references could trigger XXE in downstream XML parsers that consume the output - **requireWellFormed bypass**: The existing well-formedness checks are incomplete and provide a false sense of security

## Fix Applied

Under `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 `>`, `?`, or whitespace in the target is now refused.\ On 0.9.12 this replaces an earlier check that already rejected a colon or `xml`, so the no-colon rule is preserved.\ 0.8.15 had no processing-instruction target check at all, so the whole target validation is new there.\ Non-breaking and opt-in. See the [XML `Name` production](https://www.w3.org/TR/xml/#NT-Name). > **⚠ Opt-in required.** Protection is not automatic. Existing serialization calls remain > vulnerable unless `{ requireWellFormed: true }` is explicitly passed. Applications that > serialize untrusted DOM content should audit all `serializeToString()` call sites and add it.

### Proof of Concept - fixed path

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

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

// PI target containing > breaks the PI boundary const pi = doc.createProcessingInstruction('a>', 'data'); doc.documentElement.appendChild(pi);

// Default path: emits the ill-formed target verbatim. console.log(serializer.serializeToString(doc)); // Output: <root><?a> data?></root>

// Opt-in path: the target check now rejects the break-out character. try { serializer.serializeToString(doc, { requireWellFormed: true }); } catch (e) { console.log(e.name); // InvalidStateError } ```

### Why the default stays verbatim

W3C 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).)

### Residual limitation

The 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).

Are you affected?

Enter the version of the package you're using.

Affected packages

npm/@xmldom/xmldom
Introduced in: 0.7.0Fixed in: 0.8.15
Fixnpm install @xmldom/xmldom@0.8.15
npm/@xmldom/xmldom
Introduced in: 0.9.0Fixed in: 0.9.12
Fixnpm install @xmldom/xmldom@0.9.12
npm/xmldom
Introduced in: 0

No fixed version published yet for xmldom (npm). Pin to a known-safe version or switch to an alternative.

References