Skip to content

Hosted and vendored scans from a Bun workspace member with a stray bun.lock / bun.lockb pin that ignored lock, exit 0, and lock-only VEX attests not_affected while Bun installs the unpatched package #1101

Description

[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).

Summary

This is the Bun variant of #1094. The fix for #1094, PR #1095, deliberately leaves Bun out: its comment says a member holding "a lock its own manager reads (pnpm, yarn, Bun, vlt…)" keeps the own-lock shortcut, and the #1094 thread says the Bun variant was kept out of scope because it wasn't tested. Tested here with real Bun: Bun never reads a lock inside a workspace member. bun install at the root, and bun install run inside the member, both walk up to the workspace root and install the member from the root's bun.lock. A stray packages/a/bun.lock or bun.lockb, for example left behind when a package moved into a monorepo, is dead weight.

On main, the #884 / #901 member refusal (redirect_workspace_lockfile_elsewhere) is skipped whenever the member directory holds any npm-family lock (has_own_npm_family_lock, which includes bun.lock and bun.lockb). So a hosted scan or get <uuid> from such a member:

  • rewrites the member's ignored bun.lock / bun.lockb with the hosted pin,
  • reports status: success, redirected: 1, exit 0, with no warning,
  • leaves the root bun.lock untouched, so every bun install --frozen-lockfile (at the root or in the member) installs the unpatched package,
  • and socket-patch vex from the member then attests not_affected. That happens on a lockfile-only checkout, and on a hoisted install too, because the member has no node_modules of its own and vex falls back to the member lock.

scan --mode vendored does the same thing: it vendors into the ignored member lock, writes packages/a/.socket/, exits 0, and vex attests.

Impact

A signed-off VEX not_affected and a green CI step, while every install ships the vulnerable bytes. This is the same false-success shape as #884 and #1094, through a Bun lock.

Repro

A local mock of the patch API serves one free patch for left-pad@1.3.0, which prepends /* SOCKET-PATCHED */ to index.js. sp is socket-patch … --api-url <mock> --org o --api-token fake --patch-server-url <mock>.

mkdir -p stray ws/packages/a && cd stray
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
bun install                                   # stray/bun.lock (or bun.lockb with saveTextLockfile=false)
cd ../ws
echo '{"name":"root","private":true,"workspaces":["packages/*"]}' > package.json
printf '[install]\nlinker = "hoisted"\n' > bunfig.toml   # optional; isolated reproduces too (see the matrix)
cp ../stray/package.json packages/a/ && bun install       # the root bun.lock governs the member
cp ../stray/bun.lock packages/a/                          # the stray member lock Bun ignores
printf 'node_modules\n' > .gitignore && git init -q && git add -A && git commit -qm init

cd packages/a
sp scan --mode hosted --json --yes   # exit 0, status success, redirect.redirected 1, rewrittenFiles ["bun.lock"] (the member's)
git -C ../.. status --short          #  M packages/a/bun.lock      (root bun.lock untouched)
git clone -q ../.. /tmp/fresh && cd /tmp/fresh && bun install --frozen-lockfile --cache-dir "$(mktemp -d)"
head -c 21 node_modules/left-pad/index.js   # upstream bytes, no marker
# the same from inside the member dir: cd packages/a && bun install --frozen-lockfile → upstream bytes
cd <ws>/packages/a && sp vex -O v.json      # exit 0, one statement: not_affected (inline_mitigations_already_exist)

get <uuid> --mode hosted from the member gives the same result (success, rewrittenFiles: ["bun.lock"]).

Expected vs actual

  • Expected: CLI_CONTRACT (redirect_workspace_lockfile_elsewhere row) 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 Bun the governing lock is always the workspace root's, whatever sits in the member, so the refusal should fire (or at least a loud warning that names the stray lock). Vendored should refuse the member (vendor_lockfile_missing) as it does without the stray file. vex must not attest a pin Bun never reads.
  • Actual: the refusal is skipped because the member has "a lock of its own". Exit 0, success, and a not_affected attestation, while Bun installs unpatched.

Matrix (Linux, main fe8455d)

Bun root lock / member stray lock linker mode member run fresh frozen install (root and member) vex from member
1.2.23 text v1 / text v1 hoisted (default) hosted exit 0, member lock pinned unpatched not_affected (exit 0)
1.2.23 text v1 / bun.lockb hoisted vendored exit 0, member lockb vendored, .socket/ written unpatched not_affected
1.3.9 text v1 / text v1 isolated (default) hosted exit 0, member lock pinned unpatched refuses not_applied (member node_modules links the root store); lockfile-only checkout attests
1.4.2 text v2 / text v2 isolated (default) hosted ×2 exit 0, member lock pinned unpatched refuses in place; lockfile-only checkout: not_affected
1.4.2 text v2 / text v2 hoisted hosted ×2 exit 0, member lock pinned unpatched not_affected
1.4.2 text v2 / text v2 isolated vendored exit 0, member lock vendored unpatched not_affected
1.4.2 text v2 / bun.lockb hoisted hosted exit 0, member lockb pinned unpatched not_affected
1.4.2 text v2 / text v2 hoisted PR #1095 head cddf38d, hosted + vendored exit 0, same unpatched not_affected

Controls: the same workspace from the root pins the root lock and a frozen install is patched (pass). macOS / Windows: not probed. The check is lock-path logic with no OS branch.

First bad version: none. Release 4.0.0 (npm @socketsecurity/socket-patch-linux-x64-gnu) also exits 0 and pins the member lock (Bun 1.4.2), so it isn't a regression.

Suspect code

  • crates/socket-patch-core/src/hosted/governing_root.rs:92: let workspace = if has_own_npm_family_lock(root) { None } else { … }. has_own_npm_family_lock (:247) counts bun.lock / bun.lockb from OWN_LOCKS (:58).
  • PR Fix npm workspace-member refusal skipped by a stray member lock (#1094) #1095 adds npm_member_stray_lock, but its other_own check (governing_root.rs:316 on the PR head) returns None whenever the member holds a Bun lock, so Bun keeps the shortcut. Bun's rule is the same as npm's: a member of a package.json workspace whose root holds bun.lock / bun.lockb is installed from the root lock. (Unlike yarn berry, Bun doesn't treat a nested lock as a separate project.)
  • The vendored lock inventory (crates/socket-patch-core/src/vendor/lock_inventory/view.rs) likewise treats the member lock as the project's own.

Related: #1094 / #1095 (npm), #884 / #901, #1097.

Probe runs: none (Linux sandbox only).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions