NodeJS/shell-quote/0.0.1
quote and parse shell commands
https://www.npmjs.com/package/shell-quote
MIT
3 Security Vulnerabilities
shell-quote: Quadratic-complexity Denial of Service in `parse()` (CWE-407)
- https://github.com/ljharb/shell-quote/security/advisories/GHSA-395f-4hp3-45gv
- https://nvd.nist.gov/vuln/detail/CVE-2026-13311
- https://github.com/ljharb/shell-quote/commit/7ff5488599d01c323514f02f5efb74088dd134ec
- https://github.com/ljharb/shell-quote/releases/tag/v1.9.0
- https://www.npmjs.com/package/shell-quote
- https://github.com/advisories/GHSA-395f-4hp3-45gv
Summary
shell-quote's parse() finalizes its token list with a reduce that uses Array.prototype.concat as the accumulator. Each prev.concat(arg) copies the entire growing array, so parse() runs in O(n²) in the number of tokens. An unauthenticated attacker who can submit a string to any code path that calls parse() on it can block the single-threaded Node.js event loop for tens of seconds with a small input — a denial of service. The trigger needs no shell metacharacters (plain space-separated words suffice), so input filters that only screen for ;, |, $, or backticks do not help.
Root cause
parse.js (lines 200–203), in parseInternal — this path runs on every parse() call:
}).reduce(function (prev, arg) { // finalize parsed arguments
// TODO: replace this whole reduce with a concat
return typeof arg === 'undefined' ? prev : prev.concat(arg);
}, []);
prev.concat(arg) allocates a new array and copies all of prev on every iteration, so producing an N-token result costs 1 + 2 + … + N = O(N²) copies. A second acc.concat(s) reduce in the module.exports wrapper (lines 211–224, reached only when env is a function) has the same shape. The maintainer's own // TODO: replace this whole reduce with a concat already flags the construct.
Proof of Concept
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');
}
Measured on shell-quote@1.8.4, Node v24:
| input (N tokens) | bytes | parse() | ratio vs prev (2× input) |
|---|---|---|---|
| 16 000 | 32 KB | 678 ms | — |
| 32 000 | 64 KB | 4 169 ms | ×6.2 |
| 64 000 | 128 KB | 14 914 ms | ×3.6 |
| 128 000 | 256 KB | 57 319 ms | ×3.8 |
Time grows ~×4 per 2× input → confirmed O(n²). A ~128 KB input blocks the event loop ~15 s; ~256 KB → ~57 s; a few hundred KB more → minutes. poc.js
Impact
parse() is synchronous on the main thread; while it copies arrays quadratically the entire event loop is blocked and the process serves no other requests. Any service that calls parse() on attacker-influenced input (command parsers, chat-ops / bot command handlers, REPLs, build-script / arg-string splitters) can be driven to a sustained DoS with a single small request. No code execution and no data disclosure — availability only.
End-to-end confirmation: a minimal HTTP server that calls parse() on the request body, hit with one POST of 'x '.repeat(32000) (~63 KB), froze for ~4.5 s. An out-of-process probe client issuing harmless GET /ping requests (normally ~1 ms) observed 27 consecutive pings stalled by up to 4374 ms during that single request — i.e. every concurrent client was denied service for the whole parse. Scaling the body to a few hundred KB extends the outage to minutes.
This is the same class as several accepted 2026 advisories for quadratic-parser DoS on untrusted input (e.g. markdown-it CVE-2026-48988, js-yaml CVE-2026-53550, python-multipart CVE-2026-53539). It is distinct from the known shell-quote command-injection issues (CVE-2021-42740, CVE-2016-10541, CVE-2026-9277), which are all in quote(), not parse().
Suggested remediation
Replace the O(n²) concat-in-reduce with a linear flatten that pushes into the accumulator instead of reallocating and copying it on every iteration. Apply the same shape to the wrapper's acc.concat(s) reduce. A defensive input-length cap on parse() is a cheap additional stop-gap.
Maintainer note (edit): the originally-suggested
Array.prototype.flat()is ES2019 / Node 11+, butshell-quotedeclaresengines: node >= 0.4, so.flat()would silently drop support for older runtimes. The fix instead flattens one-level array tokens withforEach/push— and deliberately notpush.apply(...), since spreading a large array into function arguments can exceed the engine's argument count limit. Output is byte-identical to the current code across strings,undefinedholes, one-level array tokens, and{op}/{comment}/{op:'glob'}objects, and finalizing is now linear (1,024,000 tokens in ~150 ms vs ~57 s for 128,000 before). Thanks for the clear report and PoC — the analysis and reproduction were spot on.
Disclosure
Found by source audit + wall-clock confirmation against 1.8.4 (and verified the same code is present on main). Reported privately here; no public disclosure until a fix is available.
Potential Command Injection in shell-quote
Affected versions of shell-quote do not properly escape command line arguments, which may result in command injection if the library is used to escape user input destined for use as command line arguments.
Proof of Concept:
The following characters are not escaped properly: >,;,{,}
Bash has a neat but not well known feature known as Bash Brace Expansion
, wherein a sub-command can be executed without spaces by running it between a set of {} and using the , instead of to seperate arguments. Because of this, full command injection is possible even though it was initially thought to be impossible.
const quote = require('shell-quote').quote;
console.log(quote(['a;{echo,test,123,234}']));
// Actual "a;{echo,test,123,234}"
// Expected "a\;\{echo,test,123,234\}"
// Functional Equivalent "a; echo 'test' '123' '1234'"
Recommendation
Update to version 1.6.1 or later.
Potential Command Injection
The npm module shell-quote
cannot correctly escape >
and <
operator used for redirection in shell. I'm wondering if this might be possible vulnerability for many application which depends on shell-quote.
For example:
const quote = require('shell-quote').quote; console.log(quote(['foo>bar']));
will print foo>bar
, where foo\>bar
is desirable.
This module is downloaded more than 1M times per month and many other modules are depending on this. If an application is escaping command-line args with this module, they might be vulnerable from malicious user input.
For example: ``` var sq = require('../tests/get/shell-quote-1.6.0'); var exec = require('child_process').exec;
var pattern = process.argv[2];
command = sq.quote(['grep', pattern])); exec('cat file | ' + command, function ( err, stdout, stderr) { console.log(command, stdout);
}); ``` will be vulnerable when user input something like pattern = ':</etc/passwd', which causes the content of /etc/passwd to be leaked.
Internally, (Jon Lamendola, Nick Starke, Jacob Waddell) found that the ;, {, and } characters weren't escaped properly either. This allows for full command injection. A malicious user could input 'a;{echo,test,123,234}' to execute echo fully.
32 Other Versions
| Version | License | Security | Released | |
|---|---|---|---|---|
| 1.10.0 | MIT | 2026-07-10 - 23:31 | about 2 months | |
| 1.9.0 | MIT | 2026-06-25 - 04:47 | 2 months | |
| 1.8.4 | MIT | 1 | 2026-05-22 - 13:13 | 3 months |
| 1.8.3 | MIT | 2 | 2025-06-02 - 05:03 | over 1 year |
| 1.8.2 | MIT | 2 | 2024-11-27 - 21:33 | almost 2 years |
| 1.8.1 | MIT | 2 | 2023-04-07 - 20:56 | over 3 years |
| 1.8.0 | MIT | 2 | 2023-01-31 - 03:27 | over 3 years |
| 1.7.4 | MIT | 2 | 2022-10-13 - 16:52 | almost 4 years |
| 1.7.3 | MIT | 2 | 2021-10-21 - 06:34 | almost 5 years |
| 1.7.2 | MIT | 3 | 2019-09-01 - 07:46 | about 7 years |
| 1.7.1 | MIT | 3 | 2019-08-13 - 13:35 | about 7 years |
| 1.7.0 | MIT | 3 | 2019-08-13 - 07:52 | about 7 years |
| 1.6.3 | MIT | 3 | 2019-08-13 - 07:41 | about 7 years |
| 1.6.2 | MIT | 2 | 2019-08-13 - 07:15 | about 7 years |
| 1.6.1 | MIT | 2 | 2016-06-17 - 20:43 | about 10 years |
| 1.6.0 | MIT | 4 | 2016-04-24 - 05:53 | over 10 years |
| 1.5.0 | MIT | 4 | 2016-03-16 - 17:58 | over 10 years |
| 1.4.3 | MIT | 4 | 2015-03-08 - 03:47 | over 11 years |
| 1.4.2 | MIT | 4 | 2014-07-20 - 21:27 | about 12 years |
| 1.4.1 | MIT | 4 | 2013-12-25 - 01:00 | over 12 years |
| 1.4.0 | MIT | 4 | 2013-10-18 - 01:40 | almost 13 years |
| 1.3.3 | MIT | 4 | 2013-06-24 - 12:01 | about 13 years |
| 1.3.2 | MIT | 4 | 2013-06-24 - 11:50 | about 13 years |
| 1.3.1 | MIT | 4 | 2013-05-13 - 13:48 | over 13 years |
| 1.3.0 | MIT | 4 | 2013-05-13 - 13:42 | over 13 years |
| 1.2.0 | MIT | 4 | 2013-05-13 - 12:10 | over 13 years |
| 1.1.0 | MIT | 4 | 2013-05-13 - 10:35 | over 13 years |
| 1.0.0 | MIT | 3 | 2013-05-13 - 10:27 | over 13 years |
| 0.1.1 | MIT | 3 | 2013-04-17 - 08:06 | over 13 years |
| 0.1.0 | MIT | 3 | 2013-04-15 - 04:36 | over 13 years |
| 0.0.1 | MIT | 3 | 2012-05-18 - 18:25 | over 14 years |
| 0.0.0 | MIT | 3 | 2012-05-18 - 10:42 | over 14 years |
