Skip to content

Hosted scan with pnpm gitBranchLockfile pins the stale pnpm-lock.yaml and reports success, while pnpm installs unpatched bytes from pnpm-lock.<branch>.yaml #556

Description

[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).

Summary

pnpm's gitBranchLockfile setting (git-branch-lockfile=true in .npmrc on pnpm ≤10, gitBranchLockfile: true in pnpm-workspace.yaml) makes pnpm read and write pnpm-lock.<branch>.yaml instead of pnpm-lock.yaml. Hosted mode only looks at files named exactly pnpm-lock.yaml / shrinkwrap.yaml. That causes two failures:

  1. Both locks present (the usual case: pnpm-lock.yaml committed on main, and a branch lock written once the feature branch changes deps): scan --mode hosted pins the stale pnpm-lock.yaml and reports status: success, redirected: 1. pnpm installs from pnpm-lock.feature.yaml, which still points at the registry, so a fresh pnpm install --frozen-lockfile silently installs the unpatched upstream bytes. vex honestly declines (not_applied), so it's the only signal.
  2. Only a branch lock present: the scan exits 0 with redirected: 0 and the warning redirect_pnpm_no_lockfile, whose remedy ("run pnpm install to generate one") cannot work, because pnpm will just rewrite the branch lock.

Impact

On a feature branch with this setting, the user is told the patch is pinned (and the pin lands in a file they will commit), but CI and fresh checkouts on that branch install the vulnerable package. When the branch merges, pnpm's mergeGitBranchLockfiles flow can carry the unpinned branch entry over the pinned main one.

Repro (Linux, pnpm 12.8.1; local mock of the patch API serving a patched is-number@7.0.0 tarball)

mkdir app && cd app && git init -q -b main
echo '{"name":"app","version":"1.0.0","dependencies":{"is-number":"7.0.0"}}' > package.json
printf "packages:\n  - '.'\n" > pnpm-workspace.yaml; echo node_modules > .gitignore
pnpm install && git add -A && git commit -qm init && git checkout -qb feature
printf "gitBranchLockfile: true\n" >> pnpm-workspace.yaml     # pnpm 10: echo git-branch-lockfile=true > .npmrc
pnpm add is-odd@3.0.1            # writes pnpm-lock.feature.yaml; pnpm-lock.yaml stays as on main
socket-patch scan --mode hosted --json --yes --api-url $MOCK --org test-org --api-token fake
#  -> status success, redirected 1, rewrittenFiles [pnpm-lock.yaml, pnpm-workspace.yaml]
grep -c $MOCK pnpm-lock.yaml pnpm-lock.feature.yaml   # 1 / 0
# fresh checkout, empty store:
rm -rf node_modules && pnpm install --frozen-lockfile --store-dir $(mktemp -d)
head -1 node_modules/is-number/index.js    # "/*!" (upstream), not the patch marker
socket-patch vex --output v.json           # "omitting pkg:npm/is-number@7.0.0 ... (not_applied)"

Only-branch-lock variant: git init -b feature, enable the setting before the first pnpm install. Hosted scan then gives redirected 0 plus redirect_pnpm_no_lockfile.

Expected vs actual

  • Expected: CLI_CONTRACT and docs/ecosystems.md say hosted mode pins the lock the package manager actually installs from, or refuses loudly when it can't. So hosted mode should either rewrite pnpm-lock.<current-branch>.yaml (and leave the stale lock alone, or pin both), or refuse with a dedicated warning code that names the setting.
  • Actual: it pins a file pnpm ignores on this branch, and reports success. With only the branch lock, it gives a misleading "no lockfile" remedy.

Matrix (Linux; each cell run twice)

pnpm branch lock written both-locks case only-branch-lock case
9.15.9 no (this fixture's .npmrc setting produced no branch lock) n/a: pinned pnpm-lock.yaml is the one used, patched n/a
10.34.5 yes fail: success, fresh frozen install unpatched not run
11.28.3 yes fail not run
12.8.1 yes fail fail (misleading redirect_pnpm_no_lockfile)

macOS and Windows weren't tested (no probe branch this run); the logic is filename-based, so they're likely the same. Vendored mode with this setting wasn't tested.

Suspect code

  • crates/socket-patch-core/src/formats/pnpm/hosted.rs:180 – lock_keys matches only pnpm-lock.yaml / shrinkwrap.yaml.
  • crates/socket-patch-core/src/formats/registry.rs:72 – the hosted read set lists only pnpm-lock.yaml.
  • crates/socket-patch-core/src/patch/redirect/mod.rs:806 – redirect_pnpm_no_lockfile fires without considering pnpm-lock.*.yaml.

Tested on main 61cfb9b (CLI 4.0.0).

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions