Summary
On the @xmldom/xmldom 0.8.x line, parsing an XML end tag whose name is followed by a long run
of whitespace and then a non-whitespace character triggers quadratic-time regular-expression
backtracking (ReDoS), so a single small crafted end tag stalls the Node.js event loop. It is reachable
from DOMParser.parseFromString under default options, unauthenticated, before any validity
check — an availability-only denial of service. The 0.9.x line is not affected.
Details
lib/sax.js (release-0.8.x, commit e5c1480) trims trailing whitespace from a captured end-tag name
with an unanchored global regex:
Applied to a string shaped whitespace-run + one non-whitespace char (e.g. the content of an end tag
</ … x>), the engine must, for every starting position, extend [ws]+ to the end and then fail
the $ anchor when the trailing non-whitespace char is present — classic O(n²) backtracking in the
length of the whitespace run. The trimmed substring is delimited only by indexOf('>'), so the
attacker controls its length directly.
Proof of Concept
const { DOMParser } = require('@xmldom/xmldom'); // 0.8.x
const n = 64 * 1024;
const payload = '<r></' + ' '.repeat(n) + 'x>';
console.time('parse');
new DOMParser().parseFromString(payload, 'text/xml');
console.timeEnd('parse');
Measured (Node 18) — time quadruples per doubling of the whitespace run (canonical O(n²)):
| Whitespace run |
Isolated regex |
End-to-end parseFromString (0.8.13) |
| 4 KB |
5.6 ms |
5.7 ms |
| 8 KB |
22.7 ms |
22.5 ms |
| 16 KB |
88.6 ms |
92 ms |
| 32 KB |
354 ms |
361 ms |
| 64 KB |
1434 ms |
1452 ms |
| 128 KB |
5761 ms |
— |
Impact
Availability only: a single parse of a small crafted document blocks the Node.js event loop for the
duration of the quadratic scan (≈1.4 s at 64 KB; multi-second with larger inputs). No memory
blow-up, no data exposure, no integrity impact. Because XML is routinely accepted from untrusted
sources and parsed with default options, one request can stall a server.
Affected Versions
Affected on the 0.7.x and 0.8.x lines (the trailing-whitespace trim was added in 0.7.0, present
through 0.8.14); the fix targets the 0.8.x LTS patch. The 0.9.x line rewrote end-tag parsing to
an anchored linear matcher and never had this regex, so it is not affected. No published unscoped
xmldom is affected — the vulnerable code exists only in a 0.7.0 git tag that was never released to
npm (npm view xmldom → latest = 0.6.0).
Fix Applied
Anchors the end-tag trailing-whitespace trim so it runs in linear time instead of
backtracking quadratically on a long whitespace run. Byte-identical output. Non-breaking; 0.8.x-only.
Severity note
The complexity is quadratic, not exponential, so a multi-second stall requires
tens-to-hundreds of KB of input. VA:H reflects that xmldom applies no input-size limit and the
path runs on default-options parsing, so a single unbounded parse can fully stall the event loop.
References
Summary
On the
@xmldom/xmldom0.8.xline, parsing an XML end tag whose name is followed by a long runof whitespace and then a non-whitespace character triggers quadratic-time regular-expression
backtracking (ReDoS), so a single small crafted end tag stalls the Node.js event loop. It is reachable
from
DOMParser.parseFromStringunder default options, unauthenticated, before any validitycheck — an availability-only denial of service. The
0.9.xline is not affected.Details
lib/sax.js(release-0.8.x, commite5c1480) trims trailing whitespace from a captured end-tag namewith an unanchored global regex:
lib/sax.jsline 120: https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/sax.js#L120/[ \t\n\r]+$/gApplied to a string shaped
whitespace-run + one non-whitespace char(e.g. the content of an end tag</ … x>), the engine must, for every starting position, extend[ws]+to the end and then failthe
$anchor when the trailing non-whitespace char is present — classic O(n²) backtracking in thelength of the whitespace run. The trimmed substring is delimited only by
indexOf('>'), so theattacker controls its length directly.
Proof of Concept
Measured (Node 18) — time quadruples per doubling of the whitespace run (canonical O(n²)):
parseFromString(0.8.13)Impact
Availability only: a single parse of a small crafted document blocks the Node.js event loop for the
duration of the quadratic scan (≈1.4 s at 64 KB; multi-second with larger inputs). No memory
blow-up, no data exposure, no integrity impact. Because XML is routinely accepted from untrusted
sources and parsed with default options, one request can stall a server.
Affected Versions
Affected on the
0.7.xand0.8.xlines (the trailing-whitespace trim was added in0.7.0, presentthrough
0.8.14); the fix targets the0.8.xLTS patch. The0.9.xline rewrote end-tag parsing toan anchored linear matcher and never had this regex, so it is not affected. No published unscoped
xmldomis affected — the vulnerable code exists only in a0.7.0git tag that was never released tonpm (
npm view xmldom→latest= 0.6.0).Fix Applied
Anchors the end-tag trailing-whitespace trim so it runs in linear time instead of
backtracking quadratically on a long whitespace run. Byte-identical output. Non-breaking; 0.8.x-only.
Severity note
The complexity is quadratic, not exponential, so a multi-second stall requires
tens-to-hundreds of KB of input.
VA:Hreflects that xmldom applies no input-size limit and thepath runs on default-options parsing, so a single unbounded parse can fully stall the event loop.
References