GHSA-x4fp-j954-r2f4
xmldom: End-tag Whitespace-Trim Regex ReDoS — quadratic backtracking in the 0.8.x end-tag parser
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| npm | @xmldom/xmldom | >= 0.7.0, < 0.8.15 | 0.8.15 |
Details
As written in the advisory (Markdown source, shown as plain text).
## 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:
- `lib/sax.js` line 120: https://github.com/xmldom/xmldom/blob/e5c14802592685bb872c042c54c3f73758875c85/lib/sax.js#L120
```js
/[ \t\n\r]+$/g
```
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
```js
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
External links from the advisory. They lead to third-party sites.
- https://github.com/xmldom/xmldom/security/advisories/GHSA-x4fp-j954-r2f4
- https://nvd.nist.gov/vuln/detail/CVE-2026-83619
- https://github.com/xmldom/xmldom/pull/1072
- https://github.com/xmldom/xmldom/commit/3abb0934f5a8a84d83a1f9cde0f2bd04c08b2a09
- https://github.com/xmldom/xmldom
- https://github.com/xmldom/xmldom/releases/tag/0.8.15