[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
A project can install the same name@version twice: once as a normal lock entry and once bundled inside another package's tarball (inBundle: true). The vendored and hosted rewriters handle this correctly. They rewire the normal entry and skip the bundled one with a loud warning that it "stays UNPATCHED" (vendor_bundled_instance_skipped / redirect_npm_bundled_instance_skipped). vex then attests the whole purl not_affected anyway:
- vendored: always, both in-run (
scan --mode vendored --vex) and post-install (socket-patch vex). The installed bundled copy is unpatched and there is no vendored_tree_out_of_sync warning.
- hosted: in-run (
scan --mode hosted --vex) and pre-install (lockfile-pin basis). Only the post-install hosted vex catches it: it hash-checks every installed copy and omits the purl as not_applied.
So the attestation contradicts the same run's own warning, and the verdict depends on the mode and on whether node_modules exists.
Impact
A false VEX statement: the shipped product contains an unpatched copy of the vulnerable name@version, and the OpenVEX document says not_affected / inline_mitigations_already_exist for it. Downstream scanners suppress the finding.
#189 fixed the bundled-only case (nothing gets rewired, so nothing is attested). The mixed case, one patched copy plus one bundled unpatched copy, is still open.
Repro (npm 12.1.0, Linux, main f6b7fb9e, reproduced twice)
# a parent package that bundles left-pad@1.3.0
mkdir bsrc && cd bsrc
echo '{"name":"bund","version":"1.0.0","dependencies":{"left-pad":"1.3.0"},"bundleDependencies":["left-pad"]}' > package.json
npm install && npm pack && cd ..
mkdir app && mv bsrc/bund-1.0.0.tgz app/ && cd app
echo '{"name":"app","version":"1.0.0","private":true,"dependencies":{"bund":"file:bund-1.0.0.tgz","left-pad":"1.3.0"}}' > package.json
npm install
# lock: node_modules/left-pad (registry) + node_modules/bund/node_modules/left-pad {inBundle:true}
socket-patch scan --mode vendored --yes --json --vex inrun.vex.json --api-url $MOCK --org test-org --api-token fake
# events: vendor_bundled_instance_skipped ("that copy stays UNPATCHED")
# inrun.vex.json: not_affected pkg:npm/left-pad@1.3.0
# fresh checkout (no node_modules) -> npm ci
head -c 20 node_modules/left-pad/index.js # /* SOCKET-PATCHED */
head -c 20 node_modules/bund/node_modules/left-pad/index.js # original, unpatched
socket-patch vex --output v.json --json
# status success, statement not_affected "(vendored)", warnings: []
The patch source is a local mock of the patch API modelled on crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs (batch / by-package / patches/package grant / patches/view, serving a tarball with a marker prepended to index.js). For the hosted pre-install cell, pass --patch-server-url for the mock origin so the lockfile reference is recognized.
Expected vs actual
Expected: a patch is attested only when the build consumes patched bytes. The CLI contract drops a ref when "another lock resolves the same name@version from a non-Socket source" (patched_ref_unattributable, CLI_CONTRACT.md "Contested locks"). A bundled instance in the same lock is the same situation: the build consumes unpatched bytes of that name@version from a non-Socket source, the parent's tarball. At minimum a warning is expected, since the vendored contract says a present installed tree with different bytes warns vendored_tree_out_of_sync.
Actual:
| cell |
vendored |
hosted |
in-run --vex |
not_affected ✗ |
not_affected ✗ |
vex, no node_modules (lock basis) |
not_affected ✗ |
not_affected ✗ |
vex after npm ci |
not_affected, no warning ✗ |
omitted not_applied ✓ |
OS × version
|
npm 10.9.9 |
npm 12.1.0 |
Linux, main f6b7fb9e |
reproduces (vendored) |
reproduces (vendored ×2, hosted) |
| Linux, release 4.0.0 |
|
reproduces (vendored) |
| macOS / Windows |
not run (lock/VEX logic is OS-independent) |
|
Not a regression: 4.0.0 behaves the same.
Suspect code
crates/socket-patch-core/src/vex/discover/mod.rs:574-595: contest_across_locks only contests refs from a different file (e.file != r.source_file). inBundle / bundled entries are skipped outright by the npm extractor (vex/discover/npm.rs:17-22) instead of being recorded as a same-name@version "resolved elsewhere" instance.
crates/socket-patch-core/src/vex/verify.rs:185-193: the vendored out-of-sync probe checks one package_paths.get(purl) copy (the hoisted, patched one), not every installed copy. The hosted path uses verify_hosted_copies, which checks them all.
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
A project can install the same
name@versiontwice: once as a normal lock entry and once bundled inside another package's tarball (inBundle: true). The vendored and hosted rewriters handle this correctly. They rewire the normal entry and skip the bundled one with a loud warning that it "stays UNPATCHED" (vendor_bundled_instance_skipped/redirect_npm_bundled_instance_skipped).vexthen attests the whole purlnot_affectedanyway:scan --mode vendored --vex) and post-install (socket-patch vex). The installed bundled copy is unpatched and there is novendored_tree_out_of_syncwarning.scan --mode hosted --vex) and pre-install (lockfile-pin basis). Only the post-install hostedvexcatches it: it hash-checks every installed copy and omits the purl asnot_applied.So the attestation contradicts the same run's own warning, and the verdict depends on the mode and on whether
node_modulesexists.Impact
A false VEX statement: the shipped product contains an unpatched copy of the vulnerable
name@version, and the OpenVEX document saysnot_affected/inline_mitigations_already_existfor it. Downstream scanners suppress the finding.#189 fixed the bundled-only case (nothing gets rewired, so nothing is attested). The mixed case, one patched copy plus one bundled unpatched copy, is still open.
Repro (npm 12.1.0, Linux, main
f6b7fb9e, reproduced twice)The patch source is a local mock of the patch API modelled on
crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs(batch / by-package /patches/packagegrant /patches/view, serving a tarball with a marker prepended toindex.js). For the hosted pre-install cell, pass--patch-server-urlfor the mock origin so the lockfile reference is recognized.Expected vs actual
Expected: a patch is attested only when the build consumes patched bytes. The CLI contract drops a ref when "another lock resolves the same
name@versionfrom a non-Socket source" (patched_ref_unattributable, CLI_CONTRACT.md "Contested locks"). A bundled instance in the same lock is the same situation: the build consumes unpatched bytes of thatname@versionfrom a non-Socket source, the parent's tarball. At minimum a warning is expected, since the vendored contract says a present installed tree with different bytes warnsvendored_tree_out_of_sync.Actual:
--vexnot_affected✗not_affected✗vex, nonode_modules(lock basis)not_affected✗not_affected✗vexafternpm cinot_affected, no warning ✗not_applied✓OS × version
f6b7fb9eNot a regression: 4.0.0 behaves the same.
Suspect code
crates/socket-patch-core/src/vex/discover/mod.rs:574-595:contest_across_locksonly contests refs from a different file (e.file != r.source_file).inBundle/bundledentries are skipped outright by the npm extractor (vex/discover/npm.rs:17-22) instead of being recorded as a same-name@version"resolved elsewhere" instance.crates/socket-patch-core/src/vex/verify.rs:185-193: the vendored out-of-sync probe checks onepackage_paths.get(purl)copy (the hoisted, patched one), not every installed copy. The hosted path usesverify_hosted_copies, which checks them all.