Repository navigation
Vendored npm vex and scan --mode vendored --vex attest not_affected beside a file: directory copy of the patched name@version, which npm installs unpatched #1511
Description
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
This is the vendored-mode counterpart of #1481. In an npm workspace under install-strategy=nested, member a depends on registry left-pad@1.3.0 and member b depends on left-pad through a file: directory holding the same release ("left-pad": "file:../../lpdir"). The lock then has a registry entry packages/a/node_modules/left-pad and a versionless link entry packages/b/node_modules/left-pad ("resolved": "lpdir", "link": true).
scan --mode vendored --vex vendors a's copy and attests not_affected. A standalone vex after npm ci attests not_affected too, and vendor --check passes. But b's left-pad is a symlink to lpdir, which no rewrite reaches, so it stays unpatched.
The same shape with a file: tarball or a URL copy is handled correctly in vendored mode: patched_ref_unattributable, nothing attested. On main, hosted mode refuses all three shapes, the directory one included (#1482).
Impact
The VEX says the app isn't affected while one workspace member still loads the vulnerable bytes. Nothing in the run says so: the only output is vendored_tree_out_of_sync, and its remedy ("re-run your package manager's install") can't fix this. The scan reports the link copy as vendor_workspace_member_skipped, although it's a file: dependency, not a workspace member.
Repro
You need a mock of the org patch API that serves a patch for left-pad@1.3.0 (batch / view / blob / patches/package routes). Real npm installs.
mkdir -p app/packages/a app/packages/b && cd app
echo '{"name":"app","version":"1.0.0","private":true,"workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"file:../../lpdir"}}' > packages/b/package.json
mkdir lpdir && npm pack left-pad@1.3.0 && tar xzf left-pad-1.3.0.tgz -C lpdir --strip-components=1
echo install-strategy=nested > .npmrc
npm install
socket-patch scan --mode vendored --vex vex.json --json --api-url $MOCK --org o --api-token fake --patch-server-url $MOCK
# exit 0, vex.json: not_affected
rm -rf node_modules packages/*/node_modules && npm ci
grep -c SOCKET-PATCHED packages/a/node_modules/left-pad/index.js # 1
grep -c SOCKET-PATCHED lpdir/index.js # 0 (= packages/b/node_modules/left-pad)
socket-patch vex --output vex2.json # exit 0, not_affected (warning vendored_tree_out_of_sync)
socket-patch vendor --check # exit 0, "committed artifact and wiring verified"Expected vs actual
- Expected: CLI_CONTRACT.md, "Contested locks": a reference withheld because an unreachable copy installs beside it ("an npm copy … one npm installs from a git / url /
file:spec") "is never attested". Vendored mode should drop the ref withpatched_ref_unattributable, as it already does for thefile:tarball and URL shapes and as hosted mode does since Fix npm file: copy attested by in-run VEX (#1481) #1482. - Actual: exit 0 and
not_affectedfrom both the in-run and the standalonevex.vendor --checkexits 0.
Matrix (Linux, Node 22.22.0; npm 12 on Node 24)
| npm | layout | copy shape | in-run --vex |
standalone vex after npm ci |
|---|---|---|---|---|
| 10.9.4 | nested | file: dir |
attests (twice) | attests |
| 11.6.2 | nested | file: dir |
attests | attests |
| 12.2.0 | nested | file: dir |
attests | attests |
| 10.9.4 | nested | file: tarball |
refused (pass) | — |
| 10.9.4 | nested | URL tarball | refused (pass) | — |
| 8.19.4 / 10.9.4 / 12.2.0 | hoisted (default) | file: dir |
npm dedupes onto the link, nothing vendored (pass) | exit 2 (pass) |
| 10.9.4 (hosted mode, for comparison) | nested | file: dir |
refused (pass, #1482) | refused (pass) |
First bad version
Present in published v5.0.1 and on main 887a5f7. v4.0.0 can't run this flow against the same mock, so I didn't bisect further. This looks like a gap left by #1482 rather than a regression.
Suspect code
crates/socket-patch-core/src/vex/discover/npm.rs:472-478 (drop_non_registry_installs): an entry with no version is skipped (continue), so the versionless link entry that npm records for a file: directory dependency never contests the ref. #1482 taught the hosted rewriter to resolve that identity through the link target (npm_link_source_identity, crates/socket-patch-core/src/patch/redirect/mod.rs:1412), but discovery, which vendored vex / vendor --check rely on, has no equivalent. The vendored rewriter also labels the copy vendor_workspace_member_skipped.
No probe runs: probe branches are still blocked for this routine (see ledger #302), so this was tested on Linux only.
Activity
mikolalysenko commentedon Oct 14, 2026
[agent] Claiming this issue (shared root cause: npm lock discovery skips a versionless
file:directory link entry instead of resolving its identity through the link target, as the hosted rewriter does since #1482). Branch: agent/fix-npm-vendored-file-dir-copy. Claim-ID: 2026-10-14T00:20:52Z-bdc2d7Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Draft PR: #1513
Generated by Claude Code
Don't attest npm beside a file: dir copy
Name the unpatched copy in vendor --check