highCVE-2026-83606Sep 8, 2026

CVE-2026-83606: @xmldom/xmldom Processing Instruction ReDoS

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Sending a single crafted XML document with an unterminated processing instruction and a long whitespace run to any Node.js server using @xmldom/xmldom 0.9.x can stall the event loop for seconds…

Package@xmldom/xmldom
Ecosystemnpm
Affected>= 0.9.0-beta.9, <= 0.9.10
Fixed in0.9.11
CVE-2026-83606: @xmldom/xmldom Processing Instruction ReDoS

The problem

In @xmldom/xmldom 0.9.0-beta.9 through 0.9.10, the PI production in lib/grammar.js compiles a regex whose optional tail (?:S+(Char*?))? lets a greedy separator (S+) and a lazy data group (Char*?) both match XML whitespace.

When the required closing ?> is absent, the regex engine must ultimately fail, but first it tries every way to partition the whitespace run between the two groups. That is O(n^2) in the length of the trailing whitespace. Both parsePI and parseProcessingInstruction in lib/sax.js apply this regex to the entire remaining source string, so there is no natural bound on the input size.

Proof of concept

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

javascript
const { DOMParser } = require('@xmldom/xmldom');
const n = 32 * 1024;
// unterminated PI: opens with <?p, then n spaces, never closes with ?>
const payload = '<a><?p' + ' '.repeat(n);
console.time('parse');
new DOMParser().parseFromString(payload, 'text/xml');
console.timeEnd('parse');
// Node 18: ~1 s at 32 KB, ~8 s at 64 KB (quadratic growth)

The root cause (CWE-1333) is that S+ and Char*? overlap on whitespace. When the engine cannot match the mandatory ?>, it backtracks by shrinking S+ one character at a time and re-trying Char*? for each partition, producing O(n^2) work.

The patch (PR #1039, commit 73df6b8) inserts a fixed-width negative lookahead (?!\s) immediately after S+. This forces the data capture group to start only at a non-space character, making the partition unique and eliminating all backtracking. Measured improvement: 32 KB input drops from ~7777 ms to ~1 ms.

The fix

Upgrade @xmldom/xmldom to **0.9.11** or later. The 0.8.x LTS line and the unscoped xmldom package use an indexOf('?>')-bounded PI code path and are not affected by this specific issue. No configuration or workaround exists for affected 0.9.x versions short of upgrading.

Reported by jmestwa-coder.

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

Related research