Skip to content

pnpm VEX attests not_affected while a file: directory or file: tarball copy of the patched package@version in the same pnpm-lock.yaml installs unpatched, and hosted/vendored scans give no warning for that copy #935

Description

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

Summary

A pnpm workspace has one member that depends on left-pad@1.3.0 from the registry and another that depends on a file: copy of the same package: an unpacked directory (file:../../forks/left-pad) or a tarball (file:../../forks/left-pad-1.3.0.tgz). scan --mode hosted and scan --mode vendored rewire only the registry entry left-pad@1.3.0. They report success and give no warning about the file: copy, which pnpm keeps installing from the user's source.

A lockfile-only vex (no node_modules) then attests pkg:npm/left-pad@1.3.0 as not_affected in both modes. In vendored mode, vex still attests after pnpm install, even though member b's copy is unpatched. Hosted vex after the install is honest: it declines with not_applied.

This is the pnpm side of #326 (fixed for npm locks: drop_non_registry_installs), #497 (Bun) and #921 (yarn classic). CLI_CONTRACT lists the "same lock, non-Socket source contests the reference" rule (#588) for npm and Bun only.

Impact

A VEX document (the CI / compliance artefact) says the product is not affected while the product ships an unpatched copy of exactly the patched name@version. Nothing in the scan output tells the user that copy exists.

Repro (pnpm 12.8.1, main 9c43dfc)

# Mock patch API on :18601 serving pkg:npm/left-pad@1.3.0 (batch, by-package, view, package grant,
# hosted tarball, /registry/left-pad/1.3.0), same contract as tests/e2e_redirect_pnpm_build.rs.
export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18601 SOCKET_NPM_REGISTRY=http://127.0.0.1:18601/registry
mkdir -p proj/packages/a proj/packages/b proj/forks && cd proj
npm pack left-pad@1.3.0 && tar xzf left-pad-1.3.0.tgz && mv package forks/left-pad   # unmodified upstream 1.3.0
echo '{"name":"root","version":"1.0.0","private":true}' > package.json
printf 'packages:\n  - packages/*\n' > pnpm-workspace.yaml
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"file:../../forks/left-pad"}}' > packages/b/package.json
pnpm install
socket-patch scan --mode hosted --json --yes --api-url http://127.0.0.1:18601 --org test-org --api-token x
#   status success, warnings: [redirect_pnpm_trust_lockfile] only, nothing about packages/b
rm -rf node_modules packages/*/node_modules
socket-patch vex --json --output vex.json --api-url http://127.0.0.1:18601 --org test-org --api-token x
#   exit 0: verified pkg:npm/left-pad@1.3.0 not_affected (inline_mitigations_already_exist)
pnpm install --frozen-lockfile --store-dir "$(mktemp -d)"
head -1 packages/a/node_modules/left-pad/index.js   # /* SOCKET-PATCHED */
head -1 packages/b/node_modules/left-pad/index.js   # upstream header: UNPATCHED

The lock after the scan:

packages:
  left-pad@1.3.0:
    resolution: {integrity: sha512-<patched>, tarball: http://127.0.0.1:18601/patch/npm/left-pad/1.3.0/<token>/<uuid>/left-pad-1.3.0.tgz}
  left-pad@file:forks/left-pad:
    resolution: {directory: forks/left-pad, type: directory}

The tarball variant's entry carries the version explicitly, so a lock-only reader can match it:

  left-pad@file:forks/left-pad-1.3.0.tgz:
    resolution: {integrity: sha512-XI5M…(upstream), tarball: file:forks/left-pad-1.3.0.tgz}
    version: 1.3.0

In vendored mode the scan writes overrides: {left-pad@1.3.0: file:.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz}. pnpm applies it only to the registry edge, so member b keeps left-pad@file:forks/left-pad.

Expected vs actual

  • Expected: CLI_CONTRACT (VEX, "Contested locks"): when another entry of the same lock resolves the wired name@version from a non-Socket source, the reference is dropped with patched_ref_unattributable, because "the package manager installs both entries, and that copy stays unpatched". The npm row (lock table) skips "any entry npm installs from a git, URL or file: spec, together with every ref for the same name@version". pnpm installs file: directory and tarball deps from the user's spec in exactly the same way. The scan should also warn about the unreached copy, as Bun / vlt / vendored do for bundled copies (*_bundled_instance_skipped).
  • Actual: no warning from either scan. Lock-only vex attests not_affected in hosted and vendored mode. Vendored vex still attests after the install.

OS × version

Linux, Node 22, real pnpm installs, cold store per install, fresh copy of the working tree for the frozen install:

pnpm (lock) Variant Mode Scan warns? Lock-only vex b's installed copy vex after install
8.15.9 (6.0) dir hosted no not_affected unpatched declines (not_applied)
9.15.9 (9.0) dir / tgz hosted no not_affected unpatched declines
10.34.5 dir hosted no not_affected unpatched declines
11.28.3 dir hosted no not_affected unpatched declines
12.8.1 dir / tgz hosted no not_affected unpatched declines
9.15.9 dir vendored no not_affected unpatched not_affected
12.8.1 dir vendored no not_affected unpatched not_affected

Each cell was run at least once, and 12.8.1 dir/hosted twice. macOS and Windows weren't probed; the behaviour sits in lock parsing and isn't OS-specific.

First bad version: release 4.0.0 has no manifest-less lockfile VEX (vex there fails manifest_not_found on this checkout), so this isn't a regression of a shipped feature.

Suspect code

  • crates/socket-patch-core/src/vex/discover/npm.rs:602: a pnpm packages: entry without a tarball (a {directory: …} resolution) only calls resolved_elsewhere(file, pnpm_registry_key_purl(key)), and pnpm_registry_key_purl returns None for a name@file:… key. The file: tarball entry takes the same route at :644. Nothing like npm's drop_non_registry_installs (:334) / the same-lock unwired check in push_uncontested (:128) exists for pnpm. resolved_elsewhere also only contests across different locks (discover/mod.rs:593).
  • For a directory entry, the version isn't in the lock. The name is (left-pad@file:…), and the directory's package.json is in the checkout. Treating any same-name non-registry entry as contesting the ref would be the conservative fix.
  • The hosted / vendored pnpm rewriters (patch/redirect, vendor) skip the file: entry silently. Run 17 of the pnpm ledger already noted redirect_pnpm_entry_not_found when the file: copy is the only instance; with a registry instance beside it, no warning is given at all.

No probe runs: the issue is in lock parsing, and the Linux evidence above is complete.

No activity

Activity on this issue will appear here.

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