Skip to content

chore(deps): update dependency shell-quote to v1.9.0 [security] - #1250

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-shell-quote-vulnerability
Open

chore(deps): update dependency shell-quote to v1.9.0 [security]#1250
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-shell-quote-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
shell-quote 1.8.41.9.0 age confidence

shell-quote: Quadratic-complexity Denial of Service in parse() (CWE-407)

CVE-2026-13311 / GHSA-395f-4hp3-45gv

More information

Details

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.
image
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+, but shell-quote declares engines: node >= 0.4, so .flat()
would silently drop support for older runtimes. The fix instead flattens one-level
array tokens with forEach/push — and deliberately not push.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, undefined
holes, 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.

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

ljharb/shell-quote (shell-quote)

v1.9.0

Compare Source

Commits
  • [New] add types dca6e21
  • [Dev Deps] update eslint 9aa9e8f
  • [Fix] parse: finalize tokens in linear time (GHSA-395f-4hp3-45gv) 7ff5488
  • [actions] update workflows 75e8497
  • [actions] Windows + node 4/6/7: pin eslint to 9 before install, since npm 2/3 cannot stage eslint 10@types/esrecurse 3fb739d
  • [actions] retry npm install on Windows to survive npm 2/3 staging-rename flake abe0163
  • [actions] Windows + node 5/7: install deps with a modern node b4bafa2
  • [Fix] quote: escape leading ~ to prevent shell tilde-expansion 7a76c1a
  • [Dev Deps] update auto-changelog, tape 7184b44
  • [Dev Deps] apparently jackspeak is no longer in the graph 9ba368a

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@c2fo-cibot c2fo-cibot Bot added the size/XS Denotes a PR that changes 0-9 lines label Jul 25, 2026
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 30174713655

Coverage remained the same at 96.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 764
Covered Lines: 746
Line Coverage: 97.64%
Relevant Branches: 471
Covered Branches: 450
Branch Coverage: 95.54%
Branches in Coverage %: Yes
Coverage Strength: 2928181.34 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Denotes a PR that changes 0-9 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant