[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
On pnpm 10.12+ with modulesDir set (modulesDir: deps in pnpm-workspace.yaml, or modules-dir=deps in .npmrc on pnpm 10), pnpm keeps the virtual store at <modulesDir>/.pnpm. The npm crawler never looks there (the root cause of #661). In agent mode #661 shows up as "not installed". In hosted mode it is worse: standalone socket-patch vex reads the "nothing installed" result as a lockfile-only checkout and attests the hosted pin as not_affected, while the copy pnpm installed and Node loads (deps/.pnpm/left-pad@1.3.0/...) is still the upstream, unpatched file.
The same project with the default node_modules is handled honestly: vex finds the upstream copy, omits the patch and exits 1.
Impact
A false security attestation. The usual hosted workflow is scan --mode hosted, commit, then socket-patch vex as the post-install check that the redirect warning recommends ("Run socket-patch vex after installation to verify the patched files"). With modulesDir, that check publishes not_affected for a vulnerable install, for example over a warm node_modules/store, which the same warning says can still hold upstream files.
Repro
Uses a local mock of the patch API for pkg:npm/left-pad@1.3.0 (batch, package grant, view, hosted tarball; the patch prepends a marker to index.js), like e2e_redirect_pnpm_build.rs.
export SOCKET_NO_CONFIG=1 SOCKET_NO_UPDATE_CHECK=1 SOCKET_PATCH_SERVER_URL=$API
API_ARGS="--api-url $API --org test-org --api-token fake"
mkdir p && cd p
echo '{"name":"p","version":"0.0.0","private":true,"dependencies":{"left-pad":"1.3.0"}}' > package.json
printf 'modulesDir: deps\n' > pnpm-workspace.yaml # pnpm 10: echo modules-dir=deps > .npmrc
pnpm install # -> deps/.pnpm/left-pad@1.3.0 (upstream bytes)
socket-patch scan --mode hosted --json --yes $API_ARGS # success, lock repointed (+ trustLockfile on 11+)
head -c 30 deps/.pnpm/left-pad@1.3.0/node_modules/left-pad/index.js # "/* This program is free softwa" (upstream, no marker)
socket-patch vex --json $API_ARGS --output vex.json; echo $? # 0
grep '"status"' vex.json # "not_affected", "Patched via Socket patch … (redirected)"
Control: delete the modulesDir line and repeat. vex exits 1 with no_applicable_patches (the installed upstream copy fails verification), which is the correct answer.
Expected vs actual
crates/socket-patch-cli/CLI_CONTRACT.md, "Manifest-less VEX", hosted row: "The installed copies the build consumes … are hash-verified when any exist … Installed evidence wins: hash_mismatch / not_applied are omitted. With nothing installed, a discovered reference whose lock pins the artifact … attests from that pin … because 'not installed' has to mean the crawler looked."
- Expected: the installed
deps/.pnpm copy is verified, its upstream bytes fail, and the patch is omitted (exit 1), as with the default modules dir.
- Actual: the crawler never looks in
deps/, the patch comes back package_not_found, the lockfile-basis exemption turns that into an attestation, and the run prints not_affected with exit 0.
Matrix (Linux, Node 22, main 045d7ec)
| pnpm |
modulesDir |
standalone vex over the upstream install |
| 9.15.9 |
modules-dir=deps |
pass (store stays in node_modules/.pnpm; exit 1, nothing attested) |
| 10.11.1 |
modules-dir=deps |
pass (store stays in node_modules/.pnpm) |
| 10.12.0 |
modules-dir=deps |
fail: not_affected, exit 0 |
| 10.34.5 |
modules-dir=deps |
fail (reproduced 3×) |
| 11.28.3 |
modulesDir: deps |
fail (3×) |
| 12.8.1 |
modulesDir: deps |
fail (4×) |
| 10.34.5 / 12.8.1 |
default |
pass (exit 1, nothing attested) |
Release 4.0.0 (npm @socketsecurity/socket-patch) is honest in every modulesDir cell (exit 1, package_not_found). macOS and Windows are untested, but the crawler logic isn't OS-specific.
Not affected, for comparison: a transitive dep under a custom virtualStoreDir: .vstore (pnpm 10.34.5 / 12.8.1) is still found, so vex is honest there.
Separate and documented: in-run scan --mode hosted --vex attests from this run's records without hash verification (contract table, (redirected) row), so it says not_affected in both layouts. This issue is about the post-install standalone vex.
First bad commit
Bisected between v4.0.0 and 045d7ec (oracle: standalone vex on the 12.8.1 modulesDir fixture above): cf8150b "feat(vex): manifest-less VEX from hosted/vendored lockfiles … (#251)", which added the nothing-installed lockfile-basis attestation.
Suspect code
Related: #661 (agent apply, same blind spot), #686 (the same false-attestation shape for Composer vendor-dir).
No probe runs: macOS/Windows probes are on hold (see ledger #303).
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
On pnpm 10.12+ with
modulesDirset (modulesDir: depsinpnpm-workspace.yaml, ormodules-dir=depsin.npmrcon pnpm 10), pnpm keeps the virtual store at<modulesDir>/.pnpm. The npm crawler never looks there (the root cause of #661). In agent mode #661 shows up as "not installed". In hosted mode it is worse: standalonesocket-patch vexreads the "nothing installed" result as a lockfile-only checkout and attests the hosted pin asnot_affected, while the copy pnpm installed and Node loads (deps/.pnpm/left-pad@1.3.0/...) is still the upstream, unpatched file.The same project with the default
node_modulesis handled honestly: vex finds the upstream copy, omits the patch and exits 1.Impact
A false security attestation. The usual hosted workflow is
scan --mode hosted, commit, thensocket-patch vexas the post-install check that the redirect warning recommends ("Runsocket-patch vexafter installation to verify the patched files"). WithmodulesDir, that check publishesnot_affectedfor a vulnerable install, for example over a warmnode_modules/store, which the same warning says can still hold upstream files.Repro
Uses a local mock of the patch API for
pkg:npm/left-pad@1.3.0(batch, package grant, view, hosted tarball; the patch prepends a marker toindex.js), likee2e_redirect_pnpm_build.rs.Control: delete the
modulesDirline and repeat.vexexits 1 withno_applicable_patches(the installed upstream copy fails verification), which is the correct answer.Expected vs actual
crates/socket-patch-cli/CLI_CONTRACT.md, "Manifest-less VEX", hosted row: "The installed copies the build consumes … are hash-verified when any exist … Installed evidence wins:hash_mismatch/not_appliedare omitted. With nothing installed, a discovered reference whose lock pins the artifact … attests from that pin … because 'not installed' has to mean the crawler looked."deps/.pnpmcopy is verified, its upstream bytes fail, and the patch is omitted (exit 1), as with the default modules dir.deps/, the patch comes backpackage_not_found, the lockfile-basis exemption turns that into an attestation, and the run printsnot_affectedwith exit 0.Matrix (Linux, Node 22, main
045d7ec)modulesDirvexover the upstream installmodules-dir=depsnode_modules/.pnpm; exit 1, nothing attested)modules-dir=depsnode_modules/.pnpm)modules-dir=depsnot_affected, exit 0modules-dir=depsmodulesDir: depsmodulesDir: depsRelease 4.0.0 (npm
@socketsecurity/socket-patch) is honest in everymodulesDircell (exit 1,package_not_found). macOS and Windows are untested, but the crawler logic isn't OS-specific.Not affected, for comparison: a transitive dep under a custom
virtualStoreDir: .vstore(pnpm 10.34.5 / 12.8.1) is still found, so vex is honest there.Separate and documented: in-run
scan --mode hosted --vexattests from this run's records without hash verification (contract table,(redirected)row), so it saysnot_affectedin both layouts. This issue is about the post-install standalonevex.First bad commit
Bisected between v4.0.0 and
045d7ec(oracle: standalonevexon the 12.8.1modulesDirfixture above):cf8150b"feat(vex): manifest-less VEX from hosted/vendored lockfiles … (#251)", which added the nothing-installed lockfile-basis attestation.Suspect code
crates/socket-patch-cli/src/commands/vex.rs:601-610: apackage_not_foundfailure for alockfile_basispurl is excused intoappliedwhenever its ecosystem was crawled. "Crawled" is taken to mean "looked everywhere", but the npm crawler misses pnpm'smodulesDir.crates/socket-patch-core/src/crawlers/npm_crawler.rs:45configured_install_roots: it knows yarn's--modules-folderand Rush'scommon/temp, but not pnpm'smodulesDir/modules-dir(shared root cause with Agent mode ignores pnpm'smodulesDir: on pnpm 10.12+ every installed package is "not installed", and apply exits 0 leaving it unpatched #661). Fixing Agent mode ignores pnpm'smodulesDir: on pnpm 10.12+ every installed package is "not installed", and apply exits 0 leaving it unpatched #661's crawler gap would close this one too. Until then, a lockfile-basis attestation could also be withheld when a pnpmmodulesDirsetting is present.Related: #661 (agent apply, same blind spot), #686 (the same false-attestation shape for Composer
vendor-dir).No probe runs: macOS/Windows probes are on hold (see ledger #303).