Repository navigation
Yarn classic VEX still attests a pre-v5 hosted pin that yarn installs without the patched package's dependencies: #1463 checks only that the dependency's name is installed, and lock-only vex doesn't check at all #1538
Description
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
#1463 (the fix for #1462) makes a hosted rescan flag a yarn.lock pin written by v4.0.0 when the patched package.json declares a dependency the lock doesn't lock. It reports redirect_yarn_classic_dep_manifest_unlocked + redirect_unconfirmed, and vex omits the installed copy with dependency_not_installed. Three VEX paths still attest such a pin, though:
- Changed range, installed (
vexandscan --vex). The patch changes an existing dependency's range (is-odd@3.0.1:is-number ^6.0.0→^7.0.0), and the lock has nois-number@^7.0.0. The rescan warns that the pin "is not confirmed … installs the patched package without them", and emitsredirect_unconfirmed.yarn install --frozen-lockfileinstalls the patched is-odd with is-number 6.0.0. Yet standalonevexattestsnot_affected(exit 0), and so does the samescan --mode hosted --vexrun that reportedredirect_unconfirmed(only the genericvex_hosted_unverifiedwarning).unresolved_npm_dependencyonly asks whethernode_modules/is-numberexists, not whether it satisfies the range the patched package declares. - Added dependency, lock-only
vex. For the original Hosted yarn classic rescan and VEX accept a pin written by v4.0.0 whose patched package.json adds a dependency the lock doesn't have, so yarn installs the patched package without it #1462 shape (the patch addsleft-pad ^1.3.0, which the lock doesn't have), installedvexnow omits it correctly (dependency_not_installed, exit 1). Butvexon a lockfile-only checkout of the same commit (package.json + yarn.lock, with--patch-server-url) attestsnot_affected, exit 0. The dependency check only runs against an installed copy, and lock-only VEX doesn't compare the pinned block'sdependencies:sub-map with the patched manifest. - Changed range, lock-only
vex. Same as 2:not_affected, exit 0.
Impact
This is the #1462 failure mode, still open on these paths. A security patch that bumps a dependency range (for example to pull in a fixed transitive) installs with the old dependency, while CI gets a not_affected statement. Lock-only VEX is what a pipeline generates before (or without) an install. For a pin written by v4.0.0 (npm's long-time latest), nothing in v5 makes these paths fail closed.
Repro (Linux, yarn 1.22.22, local mock patch API)
The mock serves an is-odd@3.0.1 patch. Its record lists package/index.js and package/package.json; the patched package.json has "dependencies": {"is-number": "^7.0.0"} (upstream: ^6.0.0). The grant carries sha512 + sha1. $API = SOCKET_API_URL / SOCKET_PROXY_URL / SOCKET_PATCH_SERVER_URL pointing at the mock.
mkdir p && cd p
echo '{"name":"p","version":"1.0.0","private":true,"dependencies":{"is-odd":"3.0.1"}}' > package.json
yarn install
npx @socketsecurity/socket-patch@4.0.0 scan --mode hosted --yes # v4 pins is-odd; sub-map stays is-number "^6.0.0"
socket-patch scan --mode hosted --vex inrun.json --json --yes # main: exit 0, redirect_yarn_classic_dep_manifest_unlocked
# ("an earlier run's pin of it is not confirmed"),
# event skipped/redirect_unconfirmed … but inrun.json: not_affected
rm -rf node_modules && yarn install --frozen-lockfile # exit 0; node_modules/is-odd patched, node_modules/is-number 6.0.0
socket-patch vex -O v.json --json # exit 0, is-odd not_affected
mkdir ../lo && cp package.json yarn.lock ../lo && cd ../lo
socket-patch vex -O v.json --json --patch-server-url $MOCK # exit 0, is-odd not_affectedFor the added-dependency shape, serve "dependencies": {"is-number": "^6.0.0", "left-pad": "^1.3.0"} instead. Then installed vex and scan --vex omit it correctly (exit 1, dependency_not_installed / vex_omitted), but the lock-only vex still exits 0 not_affected.
Expected vs actual
- Expected: CLI_CONTRACT (hosted mode) says "a granted reference whose rewriter found nothing to edit is neither counted nor attested", and Fix hosted yarn classic rescan of v4 pins (#1462) #1463 states that a refused stale pin "is never confirmed … and in-run
scan --vexdoesn't attest it". Thedependency_not_installedrow describes the omission for a dependency the lock "never locked". A pin whose patched package needsis-number@^7.0.0, when the lock only provides^6.0.0, falls under the same rule. On a lock-only checkout, the lock itself shows the missing descriptor: the pinned block'sdependencies:sub-map doesn't match the patched manifest, and no block is keyed by it. - Actual: cases 1–3 above attest
not_affectedwith exit 0.
OS × version (main a3f56f7, Linux; lock pinned by v4.0.0)
| yarn | shape | rescan | frozen install | vex installed |
scan --vex in-run |
vex lock-only |
|---|---|---|---|---|---|---|
| 1.7.0 | range ^6→^7 | unconfirmed + warning | exit 0, is-number 6.0.0 | not_affected | not run | not_affected |
| 1.7.0 | add left-pad | unconfirmed + warning | exit 0, left-pad missing | omitted (ok) | not run | not_affected |
| 1.10.1 | range ^6→^7 | unconfirmed + warning | exit 0, is-number 6.0.0 | not_affected | not run | not_affected |
| 1.10.1 | add left-pad | unconfirmed + warning | exit 0, left-pad missing | omitted (ok) | not run | not_affected |
| 1.22.22 | range ^6→^7 | unconfirmed + warning | exit 0, is-number 6.0.0 | not_affected (×2) | not_affected (×2) | not_affected (×2) |
| 1.22.22 | add left-pad | unconfirmed + warning | exit 0, left-pad missing | omitted (ok, ×2) | omitted (ok, ×2) | not_affected (×2) |
Controls on the same main:
- A fresh v5 hosted scan of the range shape refuses to pin (
redirect_yarn_classic_dep_manifest_unlocked), and vendored refuses (vendor_dep_manifest_unlocked). - When
is-number@^7.0.0is already locked, hosted and vendored both rewrite the sub-map (…_dep_manifest_rewritten), and the frozen install puts is-number 7.0.0 under is-odd. That's correct.
So only the pre-v5 pin path is affected. It isn't platform-specific (pure lock and manifest logic), so I didn't probe macOS or Windows.
First bad: not a regression. v5.0.1 and earlier attest every one of these cells (#1462). #1463 fixed installed VEX for added dependencies only.
Suspect code
crates/socket-patch-core/src/vex/verify.rs:515unresolved_npm_dependency: it checks only thatnode_modules/<name>exists (node_resolves), never that the installed version satisfies the range the patchedpackage.jsondeclares.- The in-run
scan --vexpath still emits a statement for a patch the same run reported asredirect_unconfirmedviarefused_yarn_classic_uuids(patch/redirect/mod.rs~4143). The unconfirmed set doesn't seem to reach the in-run VEX builder; the added-dependency shape is only omitted there thanks to the installed-copy check. - Lock-only hosted VEX for yarn classic doesn't run the Yarn classic: a patch that adds a dependency to the package's own package.json leaves vendored yarn.lock with a dangling dependency (offline frozen install fails, lock churns), and hosted silently installs without it #591 comparison (
formats/yarn/classic_deps.rsunlocked_descriptors_in/dep_maps_match) between the pinned block and the record's patchedpackage.json.
No probe runs: nothing here depends on the platform.
Activity
mikolalysenko commentedon Oct 14, 2026
[agent] Triaged: priority:p1 (Yarn classic, npm family). Follows #1462 / #1463, which fixed only installed VEX for added dependencies. The three remaining paths (installed range check in
vex/verify.rsunresolved_npm_dependency, in-runscan --vexignoring the unconfirmed set, and lock-only VEX skipping theclassic_depscomparison) are still open on main. Not a duplicate; no open PR addresses it.Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Claiming this issue (shared root cause: yarn classic hosted VEX never compares a pinned block's dependencies with the patched package.json's ranges). Branch: agent/fix-yarn-classic-vex-dep-ranges. Claim-ID: 2026-10-14T20:20:58Z-08bf27
Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Draft PR: #1548
Generated by Claude Code
Check installed npm deps against declared range
Check yarn classic lock deps in lock-only VEX
Tighten #1538 checks after code review
Match override keys by their last selector
Ignore no-op npm overrides in the range check
Only count overrides that reach the patched copy
Scope parent/**/dep resolutions to the parent