{"id":"CVE-2026-13311","aliases":["GHSA-395f-4hp3-45gv"],"title":"shell-quote: Quadratic-complexity Denial of Service in `parse()` (CWE-407)","summary":"shell-quote: Quadratic-complexity Denial of Service in `parse()` (CWE-407)","severity":"high","cvss":7.5,"cwe":["CWE-407"],"vendor":"shell-quote","product":"shell-quote","ecosystem":"npm","affected":["shell-quote <= 1.8.4"],"patched":["shell-quote 1.9.0"],"published":"2026-07-20","updated":"2026-07-20","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-395f-4hp3-45gv","references":[{"url":"https://github.com/ljharb/shell-quote/security/advisories/GHSA-395f-4hp3-45gv"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-13311"},{"url":"https://github.com/ljharb/shell-quote/commit/7ff5488599d01c323514f02f5efb74088dd134ec"},{"url":"https://github.com/ljharb/shell-quote/releases/tag/v1.9.0"},{"url":"https://www.npmjs.com/package/shell-quote"},{"url":"https://github.com/advisories/GHSA-395f-4hp3-45gv"}],"tags":["ghsa","npm"],"epss":0.0036,"epssPercentile":0.29849,"ingestedAt":"2026-07-20T22:43:35.415Z","slug":"CVE-2026-13311","body":"## Overview\n\n### Summary\n`shell-quote`'s `parse()` finalizes its token list with a `reduce` that uses\n`Array.prototype.concat` as the accumulator. Each `prev.concat(arg)` copies the entire growing\narray, so `parse()` runs in **O(n²)** in the number of tokens. An unauthenticated attacker who\ncan submit a string to any code path that calls `parse()` on it can block the single-threaded\nNode.js event loop for tens of seconds with a small input — a denial of service. The trigger\nneeds **no shell metacharacters** (plain space-separated words suffice), so input filters that\nonly screen for `;`, `|`, `$`, or backticks do not help.\n\n### Root cause\n`parse.js` (lines 200–203), in `parseInternal` — this path runs on **every** `parse()` call:\n\n```js\n}).reduce(function (prev, arg) { // finalize parsed arguments\n    // TODO: replace this whole reduce with a concat\n    return typeof arg === 'undefined' ? prev : prev.concat(arg);\n}, []);\n```\n\n`prev.concat(arg)` allocates a new array and copies all of `prev` on every iteration, so\nproducing an N-token result costs `1 + 2 + … + N = O(N²)` copies. A second `acc.concat(s)`\nreduce in the `module.exports` wrapper (lines 211–224, reached only when `env` is a function)\nhas the same shape. The maintainer's own `// TODO: replace this whole reduce with a concat`\nalready flags the construct.\n\n### Proof of Concept\n```js\nconst { parse } = require('shell-quote');\nconst ms = fn => { const t = process.hrtime.bigint(); fn(); return Number(process.hrtime.bigint()-t)/1e6; };\nfor (const N of [16000, 32000, 64000, 128000]) {\n  console.log(N, 'tokens ->', ms(() => parse('x '.repeat(N))).toFixed(0), 'ms');\n}\n```\n\nMeasured on `shell-quote@1.8.4`, Node v24:\n\n| input (N tokens) | bytes  | `parse()` | ratio vs prev (2× input) |\n|-----------------:|-------:|----------:|:------------------------:|\n| 16 000           | 32 KB  |    678 ms | —                        |\n| 32 000           | 64 KB  |  4 169 ms | ×6.2                     |\n| 64 000           | 128 KB | 14 914 ms | ×3.6                     |\n| 128 000          | 256 KB | **57 319 ms** | ×3.8                 |\n\nTime grows ~×4 per 2× input → confirmed O(n²). A ~128 KB input blocks the event loop ~15 s;\n~256 KB → ~57 s; a few hundred KB more → minutes.\n<img width=\"656\" height=\"214\" alt=\"image\" src=\"https://github.com/user-attachments/assets/e8955b0e-0527-45ca-94b7-c3a2d8c0c82e\" />\n[poc.js](https://github.com/user-attachments/files/29255995/poc.js)\n\n### Impact\n`parse()` is synchronous on the main thread; while it copies arrays quadratically the entire\nevent loop is blocked and the process serves no other requests. Any service that calls `parse()`\non attacker-influenced input (command parsers, chat-ops / bot command handlers, REPLs,\nbuild-script / arg-string splitters) can be driven to a sustained DoS with a single small\nrequest. No code execution and no data disclosure — availability only.\n\nEnd-to-end confirmation: a minimal HTTP server that calls `parse()` on the request body, hit\nwith **one** `POST` of `'x '.repeat(32000)` (~63 KB), froze for ~4.5 s. An out-of-process probe\nclient issuing harmless `GET /ping` requests (normally ~1 ms) observed **27 consecutive pings\nstalled by up to 4374 ms** during that single request — i.e. every concurrent client was denied\nservice for the whole parse. Scaling the body to a few hundred KB extends the outage to minutes.\n\nThis is the same class as several accepted 2026 advisories for quadratic-parser DoS on\nuntrusted input (e.g. markdown-it CVE-2026-48988, js-yaml CVE-2026-53550,\npython-multipart CVE-2026-53539). It is **distinct** from the known `shell-quote`\ncommand-injection issues (CVE-2021-42740, CVE-2016-10541, CVE-2026-9277), which are all in\n`quote()`, not `parse()`.\n\n### Suggested remediation\nReplace the O(n²) concat-in-reduce with a linear flatten that **pushes into the\naccumulator** instead of reallocating and copying it on every iteration. Apply the\nsame shape to the wrapper's `acc.concat(s)` reduce. A defensive input-length cap on\n`parse()` is a cheap additional stop-gap.\n\n> **Maintainer note (edit):** the originally-suggested `Array.prototype.flat()` is\n> ES2019 / Node 11+, but `shell-quote` declares `engines: node >= 0.4`, so `.flat()`\n> would silently drop support for older runtimes. The fix instead flattens one-level\n> array tokens with `forEach`/`push` — and deliberately not `push.apply(...)`, since\n> spreading a large array into function arguments can exceed the engine's argument\n> count limit. Output is byte-identical to the current code across strings, `undefined`\n> holes, one-level array tokens, and `{op}`/`{comment}`/`{op:'glob'}` objects, and\n> finalizing is now linear (1,024,000 tokens in ~150 ms vs ~57 s for 128,000 before).\n> Thanks for the clear report and PoC — the analysis and reproduction were spot on.\n\n### Disclosure\nFound by source audit + wall-clock confirmation against 1.8.4 (and verified the same code is\npresent on `main`). Reported privately here; no public disclosure until a fix is available.\n\n## Affected packages\n\n- `shell-quote <= 1.8.4`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `shell-quote 1.9.0`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":41.3,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}