[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
In a yarn classic workspace where one member uses the registry left-pad@^1.3.0 and another uses a file: directory copy of the same left-pad@1.3.0 (for example a local fork or an in-repo copy), yarn.lock has two blocks:
left-pad@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a…"
integrity sha512-…
"left-pad@file:forks/left-pad":
version "1.3.0"
yarn 1 copies the file: directory into b/node_modules/left-pad, so that's a second installed copy of left-pad@1.3.0. socket-patch's own crawler counts it: agent mode patches b/node_modules/left-pad alongside the root copy.
Hosted and vendored modes rewire only the registry block. The file: block has no resolved line, so:
- Hosted / vendored
scan exit 0 with status: success, redirected: 1 and no warning that a copy stays unpatched.
- Lock-only
vex (no node_modules) attests not_affected (exit 0) in both modes. After the next yarn install --frozen-lockfile, b/node_modules/left-pad/index.js is the unpatched upstream file.
- Vendored post-install
vex still attests not_affected. Its warning tells the user to "re-run your package manager's install to resync it", but a reinstall never patches the file: copy.
- Hosted post-install
vex and agent mode behave correctly: the first omits the package (not_applied), the second patches both copies.
Impact
A VEX document claims the product isn't affected by the CVE while it ships an unpatched copy of the vulnerable package@version. Nothing in the scan output says that copy was left behind.
This is the same false claim the git-copy handling (#363 / #710) was built to prevent. It's also distinct from #758: there the lock can't show the bundled copy, but here yarn.lock names the file: block, with its version, explicitly.
Expected vs actual
docs/ecosystems.md ("yarn classic git dependencies") sets the rule for a copy the lock rewrite can't reach: the entry is left untouched with a warning naming it (redirect_yarn_classic_git_skipped), and "vex never attests the package from that lock while the git copy is there". The npm-alias rule in the same section works the same way: an unrewritable consumer gets redirect_yarn_classic_alias_skipped, because "that copy keeps the unpatched artifact". CLI_CONTRACT.md (hosted) says that "A dep counts as redirected only when its hosted-artifact URL … actually landed in a project file".
- Expected: the
file: block of the patched name@version is reported (a redirect_yarn_classic_* / vendor_* warning naming the entry), and vex doesn't attest left-pad@1.3.0 from this lock while that copy exists. This is what extract_classic already does for git_copies.
- Actual: no warning is emitted, and lock-only
vex (both modes) and post-install vex (vendored) attest not_affected.
Related, same root: when the file: block is the only left-pad@1.3.0 block ("left-pad": "file:./forks/left-pad", or yarn merging left-pad@^1.3.0, "left-pad@file:forks/left-pad": into one block with no resolved), hosted scan --json returns status: success, redirected: 0, warnings: [], skipped: []. The rewriter sets matched_any = true before finding there's no resolved line to rewrite, so it suppresses redirect_yarn_classic_entry_not_found. Only the human stderr line "no lockfile entry pinning it could be rewritten" says anything, and --json has no equivalent of it. Vendored refuses that shape with the misleading vendor_lock_entry_not_found ("make sure the package is installed and locked (yarn install)"), the same shape as #857.
Repro
This uses a local mock patch API serving a left-pad@1.3.0 patch: the self-contained run-18 mock described in the ledger, also embedded in the probe workflow on bughunt/yarn-classic/20261006-gh-shorthand.
# python3 mock.py 8787 left-pad-1.3.0.tgz & (mock from the ledger)
BIN=target/release/socket-patch; Y="yarn" # yarn 1.x
API="--api-url http://127.0.0.1:8787 --org o --api-token x"
export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:8787
mkdir -p w/forks w/a w/b && cd w
curl -sL https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz | tar xz -C forks && mv forks/package forks/left-pad
echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["a","b"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"^1.3.0"}}' > a/package.json
echo '{"name":"b","version":"1.0.0"}' > b/package.json
$Y install
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"file:../forks/left-pad"}}' > b/package.json
$Y install # yarn.lock now has the registry block + the file: block
$BIN scan --mode hosted --json --yes $API # status success, redirected 1, warnings []
rm -rf node_modules */node_modules
$BIN vex --output v.json --product pkg:npm/root@1.0.0 $API # exit 0, not_affected
$Y install --frozen-lockfile
grep -c SOCKET-PATCHED node_modules/left-pad/index.js b/node_modules/left-pad/index.js
# node_modules/left-pad/index.js:1
# b/node_modules/left-pad/index.js:0 <- unpatched copy, already attested
For vendored, use --mode vendored. Lock-only and post-install vex both attest.
OS × version
Linux, main 9c43dfc, Node 22. Each cell was run twice on 1.22.22 and once on the others.
| yarn |
hosted scan warns |
hosted lock-only vex |
hosted post-install vex |
vendored scan warns |
vendored lock-only vex |
vendored post-install vex |
| 1.0.2 |
no (bug) |
attests (bug) |
omits (ok) |
n/a (yarn ≤1.6 can't install file: tarballs) |
n/a |
n/a |
| 1.7.0 |
no (bug) |
attests (bug) |
omits (ok) |
no (bug) |
attests (bug) |
attests (bug) |
| 1.10.1 |
no (bug) |
attests (bug) |
omits (ok) |
no (bug) |
attests (bug) |
attests (bug) |
| 1.22.22 |
no (bug) |
attests (bug) |
omits (ok) |
no (bug) |
attests (bug) |
attests (bug) |
macOS and Windows weren't probed. The code path is platform-independent lock parsing.
Release v4.0.0 (1.22.22): hosted scan is silent the same way, lock-only vex omits the package (package_not_found), and post-install vex attests not_affected with the file: copy unpatched. So the defect predates v5, and v5 moved it from post-install to lock-only.
Suspect code
crates/socket-patch-core/src/vex/discover/yarn.rs:216: let Some(resolved) = resolved else { return; } drops a file: / no-resolved block of the same name@version without recording it as an unpatched copy. Compare the git_copies path at :211, which calls resolved_elsewhere and blocks attestation.
crates/socket-patch-core/src/patch/redirect/mod.rs:3342: matched_any = true is set before the rewrite. A block with no resolved line yields rewritten == *block, so there's no edit, and no redirect_yarn_classic_* warning names the copy left unpatched. The vendored yarn classic wirer has the same gap.
Found by real-yarn runs, no probe branch (Linux only).
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
In a yarn classic workspace where one member uses the registry
left-pad@^1.3.0and another uses afile:directory copy of the sameleft-pad@1.3.0(for example a local fork or an in-repo copy), yarn.lock has two blocks:yarn 1 copies the
file:directory intob/node_modules/left-pad, so that's a second installed copy of left-pad@1.3.0. socket-patch's own crawler counts it: agent mode patchesb/node_modules/left-padalongside the root copy.Hosted and vendored modes rewire only the registry block. The
file:block has noresolvedline, so:scanexit 0 withstatus: success,redirected: 1and no warning that a copy stays unpatched.vex(nonode_modules) attestsnot_affected(exit 0) in both modes. After the nextyarn install --frozen-lockfile,b/node_modules/left-pad/index.jsis the unpatched upstream file.vexstill attestsnot_affected. Its warning tells the user to "re-run your package manager's install to resync it", but a reinstall never patches thefile:copy.vexand agent mode behave correctly: the first omits the package (not_applied), the second patches both copies.Impact
A VEX document claims the product isn't affected by the CVE while it ships an unpatched copy of the vulnerable package@version. Nothing in the scan output says that copy was left behind.
This is the same false claim the git-copy handling (#363 / #710) was built to prevent. It's also distinct from #758: there the lock can't show the bundled copy, but here yarn.lock names the
file:block, with its version, explicitly.Expected vs actual
docs/ecosystems.md ("yarn classic git dependencies") sets the rule for a copy the lock rewrite can't reach: the entry is left untouched with a warning naming it (
redirect_yarn_classic_git_skipped), and "vexnever attests the package from that lock while the git copy is there". The npm-alias rule in the same section works the same way: an unrewritable consumer getsredirect_yarn_classic_alias_skipped, because "that copy keeps the unpatched artifact". CLI_CONTRACT.md (hosted) says that "A dep counts as redirected only when its hosted-artifact URL … actually landed in a project file".file:block of the patched name@version is reported (aredirect_yarn_classic_*/vendor_*warning naming the entry), andvexdoesn't attest left-pad@1.3.0 from this lock while that copy exists. This is whatextract_classicalready does forgit_copies.vex(both modes) and post-installvex(vendored) attestnot_affected.Related, same root: when the
file:block is the only left-pad@1.3.0 block ("left-pad": "file:./forks/left-pad", or yarn mergingleft-pad@^1.3.0, "left-pad@file:forks/left-pad":into one block with noresolved), hostedscan --jsonreturnsstatus: success,redirected: 0,warnings: [],skipped: []. The rewriter setsmatched_any = truebefore finding there's noresolvedline to rewrite, so it suppressesredirect_yarn_classic_entry_not_found. Only the human stderr line "no lockfile entry pinning it could be rewritten" says anything, and--jsonhas no equivalent of it. Vendored refuses that shape with the misleadingvendor_lock_entry_not_found("make sure the package is installed and locked (yarn install)"), the same shape as #857.Repro
This uses a local mock patch API serving a
left-pad@1.3.0patch: the self-contained run-18 mock described in the ledger, also embedded in the probe workflow onbughunt/yarn-classic/20261006-gh-shorthand.For vendored, use
--mode vendored. Lock-only and post-installvexboth attest.OS × version
Linux, main
9c43dfc, Node 22. Each cell was run twice on 1.22.22 and once on the others.file:tarballs)macOS and Windows weren't probed. The code path is platform-independent lock parsing.
Release v4.0.0 (1.22.22): hosted scan is silent the same way, lock-only
vexomits the package (package_not_found), and post-installvexattestsnot_affectedwith thefile:copy unpatched. So the defect predates v5, and v5 moved it from post-install to lock-only.Suspect code
crates/socket-patch-core/src/vex/discover/yarn.rs:216:let Some(resolved) = resolved else { return; }drops afile:/ no-resolvedblock of the same name@version without recording it as an unpatched copy. Compare thegit_copiespath at :211, which callsresolved_elsewhereand blocks attestation.crates/socket-patch-core/src/patch/redirect/mod.rs:3342:matched_any = trueis set before the rewrite. A block with noresolvedline yieldsrewritten == *block, so there's no edit, and noredirect_yarn_classic_*warning names the copy left unpatched. The vendored yarn classic wirer has the same gap.Found by real-yarn runs, no probe branch (Linux only).