Problem
The POSIX bin shim header resolves cygpath and wslpath through the caller's PATH. On Cygwin, MSYS2, and WSL a dependency can supply either name and control the path the shim execs.
SH_SHIM_HEADER in pnpm/crates/cmd-shim/src/shim.rs:
case `command -p uname -a` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir_win=`cygpath -w "$basedir"`
fi
exe=".exe"
msys="true"
;;
*WSL2*)
if command -v wslpath > /dev/null 2>&1; then
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
basedir_win becomes the target argument of the .exe exec branches, rendered as "$basedir_win/<target>", so whatever these report decides what node.exe runs.
A shim's own directory is node_modules/.bin, which pnpm run puts at the front of PATH, and that is where a dependency's bins live. A dependency declaring a bin named cygpath or wslpath therefore gets to answer.
Why it matters
Unlike the node.exe branch, this needs no lying uname. exe=".exe" and msys="true" are set legitimately on these hosts, so the exec branch that consumes basedir_win is the one that normally runs there. Every POSIX shim pnpm writes carries this header, so it applies to every package in every install on those platforms.
Git Bash is a common Windows development shell, so the affected population is not small.
Suggested fix
Prefer the system default path and fall back to the caller's PATH, rather than replacing the lookup outright:
if converted=$(command -p cygpath -w "$basedir" 2>/dev/null) && [ -n "$converted" ]; then
basedir_win="$converted"
elif command -v cygpath > /dev/null 2>&1; then
basedir_win=`cygpath -w "$basedir"`
fi
This is a strict improvement. When a trusted helper answers, a dependency's bin cannot stand in for it. When none does, the behavior is what it is today, so nothing that works now stops working.
Two constraints, both established by probing:
Do not fail closed. Aborting the shim when no trusted helper answers would break every bin on Git Bash if cygpath is not on the system default path there. That is a worse outcome than the hole it closes, and it is the reason this was left out of #14845.
Do not locate the helper with command -pv. It is unusable for this: in macOS /bin/sh and /bin/bash it reports a decoy on PATH rather than the system utility, while dash and zsh report the system one. Resolving a path with -pv and then executing it would defeat the hardening. The check has to attempt execution, as above.
Scope
Follow-up to #14845, which hardened readlink, sed, and uname in the same header and dropped dirname. It deliberately left these two, because command -p only helps if the helper is on the system default path, and that is not verifiable for two tools that exist only on Cygwin, MSYS2, and WSL, where no CI job runs. The fallback shape above removes that objection.
Raised in review of that PR by both Copilot and Qodo.
This wants the matching change in pnpm/cmd-shim#60. pnpm 11 generates its shims through @zkochan/cmd-shim, and pacquet keeps its copy of the header textually identical, so hardening one alone would drift the two apart.
Verification needs a host or CI job on one of those platforms, which is the other reason it did not land with #14845. pnpm/crates/cmd-shim/src/shim/tests.rs has the decoy-helper execution test to extend once there is somewhere to run it.
Written by an agent (Claude Code, claude-opus-5).
Problem
The POSIX bin shim header resolves
cygpathandwslpaththrough the caller'sPATH. On Cygwin, MSYS2, and WSL a dependency can supply either name and control the path the shim execs.SH_SHIM_HEADERinpnpm/crates/cmd-shim/src/shim.rs:basedir_winbecomes the target argument of the.exeexec branches, rendered as"$basedir_win/<target>", so whatever these report decides whatnode.exeruns.A shim's own directory is
node_modules/.bin, whichpnpm runputs at the front ofPATH, and that is where a dependency's bins live. A dependency declaring a bin namedcygpathorwslpaththerefore gets to answer.Why it matters
Unlike the
node.exebranch, this needs no lyinguname.exe=".exe"andmsys="true"are set legitimately on these hosts, so the exec branch that consumesbasedir_winis the one that normally runs there. Every POSIX shim pnpm writes carries this header, so it applies to every package in every install on those platforms.Git Bash is a common Windows development shell, so the affected population is not small.
Suggested fix
Prefer the system default path and fall back to the caller's
PATH, rather than replacing the lookup outright:This is a strict improvement. When a trusted helper answers, a dependency's bin cannot stand in for it. When none does, the behavior is what it is today, so nothing that works now stops working.
Two constraints, both established by probing:
Do not fail closed. Aborting the shim when no trusted helper answers would break every bin on Git Bash if
cygpathis not on the system default path there. That is a worse outcome than the hole it closes, and it is the reason this was left out of #14845.Do not locate the helper with
command -pv. It is unusable for this: in macOS/bin/shand/bin/bashit reports a decoy onPATHrather than the system utility, whiledashandzshreport the system one. Resolving a path with-pvand then executing it would defeat the hardening. The check has to attempt execution, as above.Scope
Follow-up to #14845, which hardened
readlink,sed, andunamein the same header and droppeddirname. It deliberately left these two, becausecommand -ponly helps if the helper is on the system default path, and that is not verifiable for two tools that exist only on Cygwin, MSYS2, and WSL, where no CI job runs. The fallback shape above removes that objection.Raised in review of that PR by both Copilot and Qodo.
This wants the matching change in pnpm/cmd-shim#60. pnpm 11 generates its shims through
@zkochan/cmd-shim, and pacquet keeps its copy of the header textually identical, so hardening one alone would drift the two apart.Verification needs a host or CI job on one of those platforms, which is the other reason it did not land with #14845.
pnpm/crates/cmd-shim/src/shim/tests.rshas the decoy-helper execution test to extend once there is somewhere to run it.Written by an agent (Claude Code, claude-opus-5).