Skip to content

Generated POSIX bin shims resolve cygpath and wslpath through the caller's PATH #14866

Description

@TrevorBurnham

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).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: supply chain securityIssues related to minimumReleaseAge, blockExoticSubdeps, build script safety, and trust policies.type: bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions