[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
A package-lock.json can wire one copy of name@version to the Socket artifact while a second entry for the same name@version in the same lock still resolves from the registry. In that state:
- vendored
vex (default and --no-verify) exits 0 and attests not_affected;
- hosted
vex --no-verify does the same;
vendor --check exits 0 with "committed artifact and wiring verified".
Yet a fresh npm ci from that lock installs the second copy unpatched. Hosted vex without --no-verify refuses correctly, because it hashes the installed copies.
The natural way to get here: vendor or scan, then add a workspace member (or any dependent) that needs the same version, and run npm install. npm resolves the new nested copy from the registry. A rescan heals it, but until then every guard says the project is fine.
Handed over by the Bun routine (#306 ledger, 20261002T133747Z entry). Re-verified here with real npm.
Impact
The VEX document claims not_affected while the build ships the vulnerable bytes. vendor --check, which is the CI drift guard, doesn't catch it either.
Repro
Main 203e092, Linux. Uses a local mock patch API: the patch is for pkg:npm/[email protected] and prepends a marker to index.js.
git init -q
echo '{"name":"app","version":"1.0.0","private":true,"workspaces":["packages/*"],"dependencies":{"is-number":"7.0.0"}}' > package.json
mkdir -p packages/a && echo '{"name":"a","version":"1.0.0","dependencies":{"is-number":"6.0.0"}}' > packages/a/package.json
npm install
socket-patch scan --mode vendored --yes # packages/a/node_modules/is-number -> file:.socket/vendor/npm/<uuid>/is-number-6.0.0.tgz
mkdir -p packages/b && echo '{"name":"b","version":"1.0.0","dependencies":{"is-number":"6.0.0"}}' > packages/b/package.json
npm install # packages/b/node_modules/is-number -> https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz
# fresh checkout: rm -rf node_modules && npm ci -> packages/b copy unpatched
socket-patch vendor --check # rc 0, "committed artifact and wiring verified"
socket-patch vex --product pkg:npm/[email protected] -O vex.json # rc 0, not_affected pkg:npm/[email protected]
Lock excerpt after the second npm install:
"packages/a/node_modules/is-number": { "resolved": "file:.socket/vendor/npm/a4f66472-…/is-number-6.0.0.tgz", … }
"packages/b/node_modules/is-number": { "resolved": "https://registry.npmjs.org/is-number/-/is-number-6.0.0.tgz", … }
Default vendored vex does print a warning, but the warning is wrong too: "the lockfile consumes it … re-run your package manager's install to resync it". Re-running npm ci doesn't help, because the lock itself is the problem.
Expected vs actual
Matrix (Linux, main 203e092)
| npm (Node) |
vendored vex |
vendored vex --no-verify |
hosted vex |
hosted vex --no-verify |
vendor --check |
| 8.19.4 (22.22) |
attests ❌ |
attests ❌ |
refuses ✅ |
attests ❌ |
— |
10.9.4 (22.22), 2 runs + fresh npm ci |
attests ❌ |
attests ❌ |
refuses ✅ |
attests ❌ |
rc 0 ❌ |
| 12.2.0 (24.21) |
attests ❌ |
attests ❌ |
refuses ✅ |
attests ❌ |
— |
The Bun routine saw the same behaviour with bun.lock. macOS and Windows weren't probed: the logic is platform-independent lock parsing. I didn't bisect: v4.0.0 can't produce a vendored lock against the same mock.
Suspect code
crates/socket-patch-core/src/vex/discover/npm.rs:127-128 (push_uncontested): contested_by requires *j != i and !wired[*j].contains(&r.purl), so an unwired copy in the lock that also carries the wired ref never contests it. Only the bundled check (lines 109-124) looks inside the same lock. vendor --check likely needs the same per-entry check.
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
A
package-lock.jsoncan wire one copy ofname@versionto the Socket artifact while a second entry for the samename@versionin the same lock still resolves from the registry. In that state:vex(default and--no-verify) exits 0 and attestsnot_affected;vex --no-verifydoes the same;vendor --checkexits 0 with "committed artifact and wiring verified".Yet a fresh
npm cifrom that lock installs the second copy unpatched. Hostedvexwithout--no-verifyrefuses correctly, because it hashes the installed copies.The natural way to get here: vendor or scan, then add a workspace member (or any dependent) that needs the same version, and run
npm install. npm resolves the new nested copy from the registry. A rescan heals it, but until then every guard says the project is fine.Handed over by the Bun routine (#306 ledger, 20261002T133747Z entry). Re-verified here with real npm.
Impact
The VEX document claims
not_affectedwhile the build ships the vulnerable bytes.vendor --check, which is the CI drift guard, doesn't catch it either.Repro
Main
203e092, Linux. Uses a local mock patch API: the patch is forpkg:npm/[email protected]and prepends a marker toindex.js.Lock excerpt after the second
npm install:Default vendored
vexdoes print a warning, but the warning is wrong too: "the lockfile consumes it … re-run your package manager's install to resync it". Re-runningnpm cidoesn't help, because the lock itself is the problem.Expected vs actual
name@versionfrom a non-Socket source … the reference is then dropped with apatched_ref_unattributablediagnostic", and it already applies this inside the same lock for bundled copies (npm VEX attests not_affected while a bundled (inBundle) copy of the same package@version stays unpatched #325) and non-registry copies (npm hosted and vendored modes rewire git-sourced lock entries, so npm ci silently installs the unpatched git bytes while scan and VEX report success #326). An unwired registry copy in the same lock installs unpatched in exactly the same way, so it should contest the reference too: no attestation, plus a "re-run scan to rewire every copy" diagnostic.vendor --checkshould report the unwired copy as drift.vendor --checkpasses.Matrix (Linux, main
203e092)vexvex --no-verifyvexvex --no-verifyvendor --checknpm ciThe Bun routine saw the same behaviour with
bun.lock. macOS and Windows weren't probed: the logic is platform-independent lock parsing. I didn't bisect: v4.0.0 can't produce a vendored lock against the same mock.Suspect code
crates/socket-patch-core/src/vex/discover/npm.rs:127-128(push_uncontested):contested_byrequires*j != iand!wired[*j].contains(&r.purl), so an unwired copy in the lock that also carries the wired ref never contests it. Only the bundled check (lines 109-124) looks inside the same lock.vendor --checklikely needs the same per-entry check.