NodeJS/shell-quote/1.6.3
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.
Improper Neutralization of Special Elements used in a Command in Shell-quote
- https://nvd.nist.gov/vuln/detail/CVE-2021-42740
- https://github.com/substack/node-shell-quote/commit/5799416ed454aa4ec9afafc895b4e31760ea1abe
- https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md#173
- https://www.npmjs.com/package/shell-quote
- https://github.com/advisories/GHSA-g4rg-993r-mgx7
- https://github.com/ljharb/shell-quote/commit/5799416ed454aa4ec9afafc895b4e31760ea1abe
- https://github.com/ljharb/shell-quote/blob/master/CHANGELOG.md#173
The shell-quote package before 1.7.3 for Node.js allows command injection. An attacker can inject unescaped shell metacharacters through a regex designed to support Windows drive letters. If the output of this package is passed to a real shell as a quoted argument to a command with exec(), an attacker can inject arbitrary commands. This is because the Windows drive letter regex character class is [A-z] instead of the correct [A-Za-z]. Several shell metacharacters exist in the space between capital letter Z and lower case letter a, such as the backtick character.
shell-quote quote() does not escape newlines in object .op values
- https://github.com/ljharb/shell-quote/security/advisories/GHSA-w7jw-789q-3m8p
- https://nvd.nist.gov/vuln/detail/CVE-2026-9277
- https://github.com/ljharb/shell-quote/commit/1518179
- https://github.com/ljharb/shell-quote
- https://www.npmjs.com/package/shell-quote
- http://www.openwall.com/lists/oss-security/2026/05/23/2
- https://github.com/advisories/GHSA-w7jw-789q-3m8p
Summary
shell-quote's quote() function did not validate object-token inputs against the operator model used by parse(). The .op field was backslash-escaped character by character using /(.)/g, which in JavaScript does not match line terminators (\n, \r, U+2028, U+2029). A line terminator in .op therefore passed through unescaped into the output; POSIX shells treat a literal \n as a command separator, so any content after it would execute as a second command.
The vulnerable code path is reachable in two ways. Neither requires the parser to misbehave — parse() only emits ops from a fixed control set — but both are documented API surface:
- Direct construction. A caller builds
{ op: '...\n...' }from external input (e.g. a deserialized argument array) and passes it toquote(). envFnreturn.parse(cmd, envFn)is documented to splice the return value ofenvFninto the result array when it is an object. An attacker-influenced data source consulted byenvFncan introduce an object token whose.opreachesquote().
Impact
Shell command injection in callers that pass object tokens with attacker-influenced .op values to quote() and then hand the result to a shell. The preconditions are narrower than ordinary string injection — they require the caller to feed object tokens into quote() — but object tokens are a public, documented part of the API surface, and quote() is intended to be a shell-safety boundary.
PoC
const { parse, quote } = require('shell-quote');
// Direct construction
quote([{ op: ';\nid' }]);
// → "\;\n\\i\\d" ← literal newline; second line executes as a command
// Via parse() with an envFn returning attacker-shaped objects
const tokens = parse('echo $X', () => ({ op: ';\nid' }));
require('child_process').execSync(quote(tokens), { shell: true });
// Executes `id` after `echo \;`.
Confirmed under sh, bash, dash, and zsh.
Patch
Fixed by replacing the per-character escape with strict shape validation in quote(). The object-token branch now:
{ op }—.opmust be a string from the same allowlist the parser emits (||,&&,;;,|&,<(,<<<,>>,>&,<&,&,;,(,),|,<,>). Anything else throwsTypeError. This is the direct fix for the reported issue and removes the entire class of.opinjection.{ op: 'glob', pattern }—.patternmust be a string with no line terminators. Glob metacharacters (*,?,[,],{,},,) pass through; all other shell-special characters are backslash-escaped. (Previously the pattern field was discarded entirely and the literal string\g\l\o\bwas emitted — a latent bug, not security-relevant.){ comment }—.commentmust be a string with no line terminators (line terminators would end the shell comment and resume command parsing — same injection shape).- Any other object shape —
TypeError.
The fix is allowlist-based rather than a targeted regex tweak, so it closes the reported vector and forecloses adjacent ones (U+2028 / U+2029 line separators in .op, line terminators in comments, unknown-shape objects coerced through .replace).
Workarounds
Prior to upgrading, callers that build object tokens from untrusted input should validate .op against the parser's operator set themselves, and never construct { op } from attacker-controlled strings.
Credits
Reported by Akshat Sinha
32 Other Versions
| Version | License | Security | Released | |
|---|---|---|---|---|
| 1.10.0 | MIT | 2026-07-10 - 23:31 | 2 months | |
| 1.9.0 | MIT | 2026-06-25 - 04:47 | 3 months | |
| 1.8.4 | MIT | 1 | 2026-05-22 - 13:13 | 4 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 | over 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 |
