Repository navigation
Hosted and vendored scans run from an npm workspace member that has a stray package-lock.json of its own rewrite that lock, which npm ignores, and report success while npm installs the unpatched package and VEX attests not_affected #1094
Description
Activity
- addedbugSomething isn't workingSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentFound by a scheduled package-manager bug-hunt agentpm:npmnpmnpm
on Oct 8, 2026 - added a commit that references this issue
on Oct 8, 2026 mikolalysenko commented
on Oct 8, 2026 CollaboratorAuthorMore actions[agent] Triage: priority:p1 (npm). Confirmed on main 05ecc6e:
governing_root::refusalskipspackage_json_workspace_refusalwheneverhas_own_npm_family_lock(root)is true, so a memberpackage-lock.json/npm-shrinkwrap.jsonsuppresses the #884 refusal even though npm installs every workspace member from the root lock. Not a duplicate of #884/#901 (those cover a member with no lock); it touches the same function as open PR #1073 but is a separate cause (the own-lock shortcut, not the glob grammar).[agent] Claiming this issue (shared root cause: npm workspace-member refusal is skipped when the member holds an npm lock that npm never reads). Branch: agent/fix-npm-member-stray-lock. Claim-ID: 2026-10-08T00:20:33Z-7c3e91
Generated by Claude Code
mikolalysenko commented
on Oct 8, 2026 CollaboratorAuthorMore actions- added a commit that references this issue
on Oct 8, 2026 mikolalysenko commented
on Oct 8, 2026 CollaboratorAuthorMore actions[agent] #1095 fixes the hosted (
scan/get) and vendored legs. A member that holds only npm locks, under apackage.jsonworkspace whose root has an npm lock, is now refused (redirect_workspace_lockfile_elsewhere/vendor_lockfile_missing) and nothing is written.The PR uses
Refs #1094rather thanFixes, and I'm leaving this issue open for one remaining piece.vexstill attests a member-lock pin that an earlier socket-patch version wrote. Gating it needs a new VEX omission code and note in CLI_CONTRACT, because the existingunattestedgate's note is Gradle-specific. I also kept the yarn classic and Bun variants out of scope, since the report didn't test them.
Generated by Claude Code
- added a commit that references this issue
on Oct 8, 2026 mikolalysenko commented
on Oct 8, 2026 CollaboratorAuthorMore actions[agent] npm bug-hunt follow-up (ledger #302), Linux, main
ea09714(after #1044 and #1073).- Still reproduces on
ea09714. I re-ran the issue's repro on npm 10.9.4: a hostedscanfrom the member exits 0 withsuccessandredirected: 1, the member's straypackage-lock.jsonis pinned, and the rootpackage-lock.jsonis untouched. - A stray member
npm-shrinkwrap.jsondoes the same. It goes through the samehas_own_npm_family_lockearly exit. I set up the member as above, but renamed its own lock tonpm-shrinkwrap.json(the root depends onleft-pad@1.2.0, so the member keeps a nestedleft-pad@1.3.0). A hostedscanfrompackages/apins the member shrinkwrap, exits 0 withsuccess/redirected: 1, and leaves the root lock unpinned. npm 7+ ignores a workspace member's shrinkwrap just as it ignores a member's package-lock, so rootnpm ciinstalls the member's copy unpatched. Reproduced twice each on npm 10.9.4 (Node 22) and npm 12.2.0 (Node 24.21).
A fix should therefore treat any npm-family lock in a member (
package-lock.jsonornpm-shrinkwrap.json) as not governing when an ancestor root lists the member underworkspaces.
Generated by Claude Code
- Still reproduces on
- added a commit that references this issue
on Oct 8, 2026 mikolalysenko commented
on Oct 9, 2026 CollaboratorAuthorMore actions[agent] Re-triage from the npm bug-hunt routine (ledger #302): fixed on main
16106b1by #1095.Linux, npm 10.9.4 / Node 22.22, local mock patch API. Workspace root
workspaces: ["packages/*"], memberpackages/adepends onms@2.1.2and has its own stray lock (npm install --package-lock-only --workspaces=false):scan --mode hostedfrompackages/a→ exit 1,Error (redirect_workspace_lockfile_elsewhere): . is a member of the npm workspace rooted at …: npm installs it from …/package-lock.json and ignores its own ./package-lock.json …; nothing was written.scan --mode vendored --cwd packages/a→ "Cannot vendor pkg:npm/ms@2.1.2: packages/a is a member of the npm workspace …", 0 vendored.- The same stray lock renamed to
npm-shrinkwrap.json→ the sameredirect_workspace_lockfile_elsewhererefusal. vexfrom the member → exit 2 (no attestation).git statusis clean after every command (nothing written).
Closing as fixed.
Generated by Claude Code
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
npm 7+ installs every workspace member from the root
package-lock.json. It never reads apackage-lock.jsoninside a member directory, and runningnpm install/npm ciinside the member walks up to the root and uses the root lock too. Stray member locks are common, for example in a package that was moved into a monorepo with its old lock still committed.The #884 / #901 workspace-member refusal (
redirect_workspace_lockfile_elsewhere) is skipped whenever the member directory holds any npm-family lock (has_own_npm_family_lock). So a hostedscan/getfrom such a member:.npmrc,status: success,redirected: 1, exit 0, with no warning,npm ci(at the root or in the member) installs the unpatched package,socket-patch vexfrom the member then attestsnot_affected.Vendored mode behaves the same way. Without the stray lock, the member is correctly refused (exit 1). With it,
scan --mode vendoredvendors into the ignored member lock, exits 0, andvexattests.Impact
The patch looks applied, and a signed-off VEX statement says
not_affected, but every install ships the vulnerable bytes. This is the same false-success shape as #884 (member with no lock) and #899 (a lock npm 12 ignores), reached through a different layout.Repro
A local mock of the patch API serves one free patch for
left-pad@1.3.0(it prepends/* SOCKET-PATCHED */toindex.js).spissocket-patch … --api-url <mock> --org o --api-token fake --patch-server-url <mock>.get <uuid> --mode hostedandscan --mode vendoredfrom the member give the same result.Expected vs actual
redirect_workspace_lockfile_elsewhere) says a hosted run from a workspace member "whose lock lives in another directory" is refused, because "the rewriters, which read only the project directory, would pin nothing". For npm the governing lock is always the root's, whatever else sits in the member. The refusal (or at least a loud warning) should fire. Vendored should keep refusing the member (vendor_lockfile_missing) as it does without the stray file.vexmust not attest a pin that npm never reads.not_affectedattestation, while npm installs unpatched.The own-lock shortcut is right for pnpm (
sharedWorkspaceLockfile: false, #492) but wrong for npm, whose workspaces never read a member lock. yarn classic / berry and Bun probably behave the same way, but that wasn't tested here.Matrix (Linux, main
05ecc6e, each cell run twice)scanget <uuid>scannpm ciinstallsvexfrom memberControl: the same workspace without
packages/a/package-lock.jsonis refused (redirect_workspace_lockfile_elsewhere/ vendored exit 1), which passes. A hosted scan from the root pins the root lock, andnpm ciinstalls patched, which also passes.First bad version: none. v4.0.0 (npm
@socketsecurity/socket-patch) also pins the stray member lock (exit 0) on npm 10.9.4, so this predates the #884 fix and isn't a regression. The #901 refusal just doesn't cover it.Suspect code
crates/socket-patch-core/src/hosted/governing_root.rs:90:let workspace = if has_own_npm_family_lock(root) { None } else { package_json_workspace_refusal(root).await }. A memberpackage-lock.json/npm-shrinkwrap.jsonskips the npm workspace check.has_own_npm_family_lockis at:242andOWN_LOCKS/NPM_LOCKSat:56.crates/socket-patch-core/src/vendor/lock_inventory/view.rs,vendor_lockfile_missing) likewise treats the member lock as the project's own.Probe runs: none (Linux sandbox only; macOS / Windows untested).