Skip to content

fix(deps): update dependency shell-quote to ^1.10.0 [security] - #736

Open
dependency-control-plane[bot] wants to merge 2 commits into
mainfrom
renovate/npm-shell-quote-vulnerability
Open

fix(deps): update dependency shell-quote to ^1.10.0 [security]#736
dependency-control-plane[bot] wants to merge 2 commits into
mainfrom
renovate/npm-shell-quote-vulnerability

Conversation

@dependency-control-plane

Copy link
Copy Markdown

This PR contains the following updates:

Package Change Age Confidence
shell-quote ^1.8.3^1.10.0 age confidence

shell-quote quote() does not escape newlines in object .op values

CVE-2026-9277 / GHSA-w7jw-789q-3m8p

More information

Details

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:

  1. Direct construction. A caller builds { op: '...\n...' } from external input (e.g. a deserialized argument array) and passes it to quote().
  2. envFn return. parse(cmd, envFn) is documented to splice the return value of envFn into the result array when it is an object. An attacker-influenced data source consulted by envFn can introduce an object token whose .op reaches quote().
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 }.op must be a string from the same allowlist the parser emits (||, &&, ;;, |&, <(, <<<, >>, >&, <&, &, ;, (, ), |, <, >). Anything else throws TypeError. This is the direct fix for the reported issue and removes the entire class of .op injection.
  • { op: 'glob', pattern }.pattern must 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\b was emitted — a latent bug, not security-relevant.)
  • { comment }.comment must be a string with no line terminators (line terminators would end the shell comment and resume command parsing — same injection shape).
  • Any other object shapeTypeError.

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

Severity

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

References

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


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 [email protected], 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.10.0

Compare Source

Merged
  • [New] parse: add opt-in splitUnquoted option for shell field-splitting of unquoted expansions #1
Commits
  • [Fix] parse: match nested ${...} braces so nested parameter expansion is consumed as one substitution c0842c8
  • [Tests] parse: pin single-quote literalness and unmatched-quote handling a0d03e3
  • [readme] remove the space in js code fences so evalmd evaluates them 2116fa3
  • [Tests] quote: pin conservative escaping of =, @, ^, ,, :, ! (#​11) 1c36f3f
  • [readme] document that quote outputs POSIX quoting, not cmd.exe/PowerShell 100e96e
  • [readme] document parse's supported parameter-expansion subset e1c75cd
  • [Fix] parse: a backslash inside single quotes must not escape the closing quote 5d460a3
  • [readme] fix stale example outputs 2de86f5
  • [Tests] quote: pin that a backslash with whitespace is not doubled in single quotes (#​14) 190e236
  • [readme] quote: use output verbatim; do not re-quote it (#​11) 1b36468
  • [Refactor] parse: fix swapped SINGLE_QUOTE/DOUBLE_QUOTE variable names 801af5c
  • [types] fix an error TS v6 ignores but v7 fails on 59bbf8b
  • [Dev Deps] update @arethetypeswrong/cli, evalmd a04d475
  • [Dev Deps] update @arethetypeswrong/ci, eslint d390f9a
  • [Tests] quote: the tilde test escapes every ~, not just a leading one (#​9) 617d119

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

v1.8.4

Compare Source

Commits
  • [Fix] quote: validate object-token shapes 4378a6e
  • [Dev Deps] update @ljharb/eslint-config, auto-changelog, eslint, npmignore 22ebec0
  • [Tests] increase coverage 9f3caa3
  • [readme] replace runkit CI badge with shields.io check-runs badge 3344a04
  • [Dev Deps] update @ljharb/eslint-config 699c511

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 becomes conflicted, 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 has been generated by Mend Renovate CLI.

@dependency-control-plane

Copy link
Copy Markdown
Author

Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

⚠️ Warning: custom changes will be lost.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants