[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
Take a lockfileVersion 2 package-lock.json, the format npm 7 and 8 write, which also carries the legacy dependencies mirror that npm 6 reads. When it holds an npm alias ("lp": "npm:left-pad@1.3.0"), hosted and vendored mode rewrite only the packages["node_modules/lp"] entry. The mirror node dependencies.lp ("version": "npm:left-pad@1.3.0") keeps its registry resolved and integrity.
- Hosted (the v5 default) does this silently. Its only warning is
redirect_npm_allow_remote.
- Vendored warns
vendor_legacy_alias_skipped ("npm 6 clients reading the v2 legacy mirror still install the UNPATCHED registry bytes through it"). See crates/socket-patch-core/src/vendor/npm_lock.rs:950-972.
In both modes, manifest-less socket-patch vex then attests the package not_affected from the lockfile. Meanwhile npm ci with npm 6 on that same committed lock installs the unpatched registry tarball and exits 0.
Non-aliased entries don't have this problem: the mirror is rewritten, and npm 6 installs the patched bytes. That holds for both hosted and vendored, and for nested entries too.
A related symptom in the same area: with an npm 6 lockfileVersion 1 lock and the same alias, hosted scan writes nothing. It warns redirect_npm_entry_not_found: no package-lock.json entry for left-pad@1.3.0 and exits 0 success, even though the lock contains the alias entry "lp": {"version": "npm:left-pad@1.3.0", ...}.
Impact
Docs list npm 6 as able to install a v2 lock (docs/testing/npm-compatibility.md, docs/ecosystems.md:56). A project that commits a v2 lock and has any npm 6 consumer (CI image, developer machine) gets the vulnerable code. Its VEX document still claims the vulnerability is not exploitable, which is the false attestation VEX must never make. Vendored at least warns at write time. Hosted gives no signal at all.
Repro
This uses a local mock of the patch API (batch / by-package / patches/package / patches/view / the tarball route), with --patch-server-url pointing at it. The patched tarball prepends /* SOCKET-PATCHED */ to index.js.
SPA="--api-url http://127.0.0.1:8765 --org o --api-token fake --patch-server-url http://127.0.0.1:8765"
mkdir repro && cd repro
echo '{"name":"t","version":"1.0.0","private":true,"dependencies":{"lp":"npm:left-pad@1.3.0"}}' > package.json
npx -y npm@8.19.4 install # lockfileVersion 2
socket-patch scan $SPA --json # hosted: status success, redirected 1, warnings: [redirect_npm_allow_remote]
node -e 'const l=require("./package-lock.json");console.log(l.packages["node_modules/lp"].resolved, l.dependencies.lp.resolved)'
# http://127.0.0.1:8765/patch/npm/<token>/<uuid>/left-pad-1.3.0.tgz https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz
# fresh checkouts of the committed files (package.json, package-lock.json, .npmrc)
(cp -r ../repro ../c6 && cd ../c6 && rm -rf node_modules && npx -y npm@6.14.18 ci && head -c 21 node_modules/lp/index.js) # "/* This program is fr" (UNPATCHED)
(cp -r ../repro ../c12 && cd ../c12 && rm -rf node_modules && npx -y npm@12.1.0 ci && head -c 21 node_modules/lp/index.js) # "/* SOCKET-PATCHED */"
(cp -r ../repro ../lo && cd ../lo && rm -rf node_modules && socket-patch vex $SPA -O v.json) # 1 statement: not_affected
To see the vendored twin, run socket-patch scan --mode vendored $SPA instead of the hosted scan. It prints the vendor_legacy_alias_skipped warning. npm 6 ci again installs unpatched bytes, and lockfile-only vex is again not_affected.
Expected vs actual
- Expected: CLI_CONTRACT.md's lockfile table says for npm lock v2: "v2 legacy
dependencies mirror … legacy mirror rewritten". So the alias mirror node should be rewritten like the non-alias ones. Failing that, the run should at least fail loudly in hosted mode (the vendored warning parity). VEX should also withhold the attestation while a lock that some supported npm reads still resolves the package from the registry. That's the same principle as patched_ref_unattributable for the dual-lock shrinkwrap/package-lock case.
- Actual: hosted rewrites only the
packages half, silently. Vendored does the same with a warning. VEX attests not_affected in both modes, while npm 6 installs the unpatched bytes.
Matrix (Linux, Node 22.22)
| Lock writer |
Mode |
npm 6.14.18 ci |
npm 12.1.0 ci |
lockfile-only vex |
npm 8.19.4 (v2), alias lp + scoped alias @x/lp |
hosted |
unpatched |
patched |
not_affected |
| npm 8.19.4 (v2), alias |
vendored |
unpatched (warned at write) |
n/a |
not_affected |
npm 8.19.4 (v2), plain is-number + nested copy (control) |
hosted |
patched |
patched |
not_affected (correct) |
| npm 6.14.18 (v1), alias |
hosted |
nothing written, redirect_npm_entry_not_found, exit 0 |
— |
no refs |
First bad version
This isn't a v5 regression. Release 4.0.0 (scan --mode hosted) leaves the same alias mirror nodes on the registry. Main 2463257 behaves the same way.
Suspect code
- Vendored:
crates/socket-patch-core/src/vendor/npm_lock.rs:950 (rewrite_legacy_tree, the vendor_legacy_alias_skipped branch).
- Hosted: the npm
package-lock.json redirect under crates/socket-patch-core/src/patch/redirect/ has no counterpart warning and no alias-mirror rewrite.
- VEX: manifest-less npm lock discovery (
crates/socket-patch-core/src/vex/discover/) attests from the packages half without checking the legacy mirror.
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
Take a lockfileVersion 2
package-lock.json, the format npm 7 and 8 write, which also carries the legacydependenciesmirror that npm 6 reads. When it holds an npm alias ("lp": "npm:left-pad@1.3.0"), hosted and vendored mode rewrite only thepackages["node_modules/lp"]entry. The mirror nodedependencies.lp("version": "npm:left-pad@1.3.0") keeps its registryresolvedandintegrity.redirect_npm_allow_remote.vendor_legacy_alias_skipped("npm 6 clients reading the v2 legacy mirror still install the UNPATCHED registry bytes through it"). Seecrates/socket-patch-core/src/vendor/npm_lock.rs:950-972.In both modes, manifest-less
socket-patch vexthen attests the packagenot_affectedfrom the lockfile. Meanwhilenpm ciwith npm 6 on that same committed lock installs the unpatched registry tarball and exits 0.Non-aliased entries don't have this problem: the mirror is rewritten, and npm 6 installs the patched bytes. That holds for both hosted and vendored, and for nested entries too.
A related symptom in the same area: with an npm 6 lockfileVersion 1 lock and the same alias, hosted
scanwrites nothing. It warnsredirect_npm_entry_not_found: no package-lock.json entry for left-pad@1.3.0and exits 0success, even though the lock contains the alias entry"lp": {"version": "npm:left-pad@1.3.0", ...}.Impact
Docs list npm 6 as able to install a v2 lock (docs/testing/npm-compatibility.md, docs/ecosystems.md:56). A project that commits a v2 lock and has any npm 6 consumer (CI image, developer machine) gets the vulnerable code. Its VEX document still claims the vulnerability is not exploitable, which is the false attestation VEX must never make. Vendored at least warns at write time. Hosted gives no signal at all.
Repro
This uses a local mock of the patch API (batch / by-package /
patches/package/patches/view/ the tarball route), with--patch-server-urlpointing at it. The patched tarball prepends/* SOCKET-PATCHED */toindex.js.To see the vendored twin, run
socket-patch scan --mode vendored $SPAinstead of the hosted scan. It prints thevendor_legacy_alias_skippedwarning. npm 6ciagain installs unpatched bytes, and lockfile-onlyvexis againnot_affected.Expected vs actual
dependenciesmirror … legacy mirror rewritten". So the alias mirror node should be rewritten like the non-alias ones. Failing that, the run should at least fail loudly in hosted mode (the vendored warning parity). VEX should also withhold the attestation while a lock that some supported npm reads still resolves the package from the registry. That's the same principle aspatched_ref_unattributablefor the dual-lock shrinkwrap/package-lock case.packageshalf, silently. Vendored does the same with a warning. VEX attestsnot_affectedin both modes, while npm 6 installs the unpatched bytes.Matrix (Linux, Node 22.22)
cicivexlp+ scoped alias@x/lpis-number+ nested copy (control)redirect_npm_entry_not_found, exit 0First bad version
This isn't a v5 regression. Release 4.0.0 (
scan --mode hosted) leaves the same alias mirror nodes on the registry. Main2463257behaves the same way.Suspect code
crates/socket-patch-core/src/vendor/npm_lock.rs:950(rewrite_legacy_tree, thevendor_legacy_alias_skippedbranch).package-lock.jsonredirect undercrates/socket-patch-core/src/patch/redirect/has no counterpart warning and no alias-mirror rewrite.crates/socket-patch-core/src/vex/discover/) attests from thepackageshalf without checking the legacy mirror.