Skip to content

npm vendored vex and vendor --check pass while a second registry copy of the patched package@version in the same package-lock.json stays unwired and installs unpatched #588

Description

[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/is-number@6.0.0 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/app@1.0.0 -O vex.json   # rc 0, not_affected pkg:npm/is-number@6.0.0

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.

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