From dd8808a0e32c68bc7a4aa2ffc4a72dfb6bdaa6fd Mon Sep 17 00:00:00 2001 From: Lakshmikanthan K Date: Mon, 30 Mar 2026 18:37:24 +0530 Subject: [PATCH] fix(core): replace unsafe cmd.exe fallback with PowerShell on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When bash is absent, zx silently fell through to child_process with shell:true — which on Windows means cmd.exe. cmd.exe does not interpret bash-style ANSI-C quoting (\$'...''), so every interpolated argument is a straight injection vector: '& calc &' passes through the quoting layer unmodified and the metacharacters are executed by cmd.exe. Fix: make the bash-not-found path Windows-aware. - Probe PATH for pwsh then powershell.exe in order of preference. - If a PS shell is found, use it with the matching quotePowerShell quoting convention and the '; exit \0' postfix. - If no safe shell is found at all on win32, throw a Fail with a clear diagnostic rather than silently executing in an insecure context. - Non-Windows platforms retain the prior (acceptable) behaviour. - Explicit shell/prefix/postfix CLI overrides are still honoured after auto-detection runs. Reported-by: LAKSHMIKANTHAN K (letchupkt) CWE: CWE-78 (OS Command Injection) --- src/core.ts | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/core.ts b/src/core.ts index 3ef7d08252..0a7ae26d05 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1022,13 +1022,40 @@ function setShell(n: string, ps = true) { $.quote = ps ? quotePowerShell : quote } -try { +{ + // Preserve any shell/prefix/postfix overrides the caller may have set + // before we attempt shell detection (set -euo pipefail etc.). const { shell, prefix, postfix } = $ - useBash() - if (isString(shell)) $.shell = shell - if (isString(prefix)) $.prefix = prefix + try { + useBash() + } catch { + // bash not found — on Windows we must NOT fall through to cmd.exe: its + // parser does not honour bash-style $'…' quoting, so every interpolated + // argument is a potential command-injection vector. + if (process.platform === 'win32') { + const winShell = which.sync('pwsh', { nothrow: true }) + ?? which.sync('powershell.exe', { nothrow: true }) + if (!winShell) { + throw new Fail( + `No safe shell found: 'bash', 'pwsh', and 'powershell.exe' are all absent from PATH. ` + + `Running under cmd.exe is unsafe because bash-style quoting does not apply there.` + ) + } + // pwsh / powershell.exe — use the PowerShell quoting convention and + // the standard ; exit $LastExitCode postfix. + $.shell = winShell + $.prefix = '' + $.postfix = '; exit $LastExitCode' + $.quote = quotePowerShell + } + // On non-Windows platforms the original behaviour (shell:true via + // execvp) is acceptable; no action needed. + } + // Re-apply explicit caller overrides now that defaults are set. + if (isString(shell)) $.shell = shell + if (isString(prefix)) $.prefix = prefix if (isString(postfix)) $.postfix = postfix -} catch (err) {} +} let cwdSyncHook: AsyncHook