Repository navigation
npm VEX attests not_affected while a bundled (inBundle) copy of the same package@version stays unpatched #325
Description
[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 novendored_tree_out_of_syncwarning. - hosted: in-run (
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_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_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.
Activity
mikolalysenko commentedon Sep 30, 2026
[agent] Triaged:
priority:p1(npm). Not a duplicate: #189 fixed the bundled-only case, and this is the mixed case. No existing fix PR. The cause is in VEX discovery and verification (bundled entries are dropped before contesting, and the vendored probe checks one copy). #326 is related but separate: that one is the rewriters selecting non-registry entries.Generated by Claude Code
mikolalysenko commentedon Sep 30, 2026
[agent] Claiming this issue (single-issue cluster; root cause: VEX discovery drops
inBundlelock entries instead of treating them as a non-Socket copy of the same name@version, and the vendored probe checks only one installed copy). Branch: agent/fix-npm-vex-bundled-copies. Claim-ID: 2026-09-30T16:20:49Z-b03cceGenerated by Claude Code
mikolalysenko commentedon Sep 30, 2026
[agent] Draft PR: #337
Generated by Claude Code
Don't attest npm patches a bundled copy bypasses
Document the bundled npm copy VEX contest
Merge main into fix-npm-vex-bundled-copies
mikolalysenko commentedon Oct 1, 2026
[agent] Re-checked on the v5 main
2463257(#277): this still reproduces with the new default hosted mode. Linux, npm 10.9.4 / Node 22.22. I used the repro above with a baresocket-patch scan(hosted) and ran it twice in fresh dirs:The behaviour is the same as on
f6b7fb9. The in-run--vexand the pre-installvexattestnot_affectedwhile the same run warns that the bundled copy stays unpatched.Generated by Claude Code
Skip bundled bun.lock entries, never attest them
mikolalysenko commentedon Oct 1, 2026
[agent] Re-check on main
61cfb9b(after #337): the hosted in-run--vexstill attests the bundled-contested purl, so I'm reopening. The other cases from the summary are fixed.Linux, npm 10.9.4, the repro from this issue (
bundbundlingleft-pad@1.3.0plus a rootleft-pad@1.3.0), patch served by a local mock of the patch API:The run's own warning says "the patch is not attested", yet
inrun.vex.jsoncontains anot_affected/inline_mitigations_already_existstatement forpkg:npm/left-pad@1.3.0. I reproduced it a second time with a registry bundle:{"npm":"10.9.4","strip-ansi":"6.0.1"}, with a patch foransi-regex@5.0.1, whichnpmbundles atnode_modules/npm/node_modules/ansi-regex. Same result. Afternpm ci, the bundled copy is the original.61cfb9bscan --vexnot_affected)vexscan --mode vendored --vexvex_claim_unwired/vex_omitted, nothing attested)The in-run hosted attestation probably comes from the run's
assume_appliedset (verified: false), which skips thepatched_ref_unattributabledrop that the lockfile-discovery path now applies.Generated by Claude Code
Fix Bun/vlt bundled copies left unpatched (#469, #471) (#472)
mikolalysenko commentedon Oct 3, 2026
[agent] Claiming this issue (single-issue cluster; root cause: the npm package-lock hosted rewriter skips
inBundle/ legacybundledcopies without recording the patch uuid inbundled_skipped_uuids, so the in-runscan --vexputs the purl inassume_appliedand attests it, while Bun's rewriter already records the uuid (#469)). Branch: agent/fix-npm-hosted-bundled-assume-applied. Claim-ID: 2026-10-03T08:21:12Z-e2606bGenerated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Draft PR: #669
Generated by Claude Code
Stop in-run VEX attesting npm bundled copies
Add changelog entry for #325
Re-bless npm lock rewrite golden for #325
Fix in-run hosted VEX attesting npm bundled copies (#325) (#669)