Skip to content

scan -g inside a Yarn Berry project runs the project's global package.json script and scans whatever directory it prints as a global install #440

Description

[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).

Summary

To find yarn's global node_modules, the npm crawler shells out to yarn global dir in the current working directory (crates/socket-patch-core/src/crawlers/npm_crawler.rs:517). Yarn Berry (2, 3 and 4) has no global command. When the cwd is inside a Berry project, Berry treats an unknown command as yarn run <name>, so yarn global dir runs the project's "global" script from package.json with the argument dir. socket-patch then takes whatever that script prints on stdout, appends /node_modules, and scans that directory as a global install.

Impact

  • Code execution from project content. Running socket-patch scan -g (or SOCKET_GLOBAL=1 socket-patch scan) from inside an untrusted checkout runs a script that checkout controls. A user would reasonably expect a global scan to read only machine-wide install locations and never run project code. No install, yarn run or lifecycle step is involved.
  • Wrong scan scope. Any directory the script prints is scanned and reported as a global install. With --mode agent / --apply, socket-patch patches files in it. This breaks the rule that -g scans only global locations.
  • Even a harmless project script with that name (for example "global": "node scripts/setup-global.js") runs as a side effect of a read-only report.

Repro (Linux, yarn 4.12.0 from @yarnpkg/cli-dist)

mkdir proj && cd proj
cat > mark.js <<'JS'
const fs = require('fs'), path = require('path');
fs.appendFileSync(path.join(__dirname, 'MARKER'), 'ran ' + process.argv.slice(2).join(' ') + '\n');
console.log(path.join(__dirname, 'fakeglobal'));   // becomes a "global" dir
JS
mkdir -p fakeglobal/node_modules/zz-project-only-pkg
echo '{"name":"zz-project-only-pkg","version":"9.9.9"}' > fakeglobal/node_modules/zz-project-only-pkg/package.json
echo '{"name":"p","scripts":{"global":"node mark.js"}}' > package.json
touch yarn.lock && printf 'nodeLinker: node-modules\n' > .yarnrc.yml
yarn install                       # yarn 2.x / 3.x / 4.x
socket-patch scan -g --json | jq .scannedPackages    # N+1
cat MARKER                         # "ran dir": the project script was executed
echo '{"name":"p"}' > package.json
socket-patch scan -g --json | jq .scannedPackages    # N

On the sandbox this reproduced on every run (well over 2): scan -g reported 552 packages with the script and 551 without, and created MARKER each time. It also happens under the PnP linker, and with SOCKET_GLOBAL=1. --global-prefix isn't affected, because it skips discovery.

Expected vs actual

  • Expected: --global operates on globally installed packages (--help: "Operate on globally-installed packages"). docs/usage.md and the CLI contract describe -g as targeting the machine tree, not the project. A global scan should read install locations and never run project-defined scripts.
  • Actual: the project's global script runs, and its stdout chooses a directory that is then scanned (and patched in agent mode) as a global install.

OS × version (probe run https://github.com/SocketDev/socket-patch/actions/runs/36828589815, main 2463257)

OS yarn 2.4.2 yarn 3.8.7 yarn 4.12.0 yarn 4.18.1
Linux (sandbox + ubuntu-latest) reproduces reproduces reproduces reproduces
macOS (macos-latest) reproduces reproduces reproduces reproduces
Windows (windows-latest) doesn't reproduce doesn't reproduce doesn't reproduce doesn't reproduce

The probe printed lines like RESULT os=macos-latest yarn=4.12.0 script_runs=1 scanned_with_script=652 scanned_without=651. On Windows script_runs=0: yarn there is an npm .cmd shim, and std::process::Command::new("yarn") doesn't resolve .cmd. That likely also means yarn's global dir is never discovered on Windows at all, but that's a separate matter.

First bad version

The published release 4.0.0 (npm @socketsecurity/[email protected]) behaves the same. The yarn global dir shell-out predates the current history of npm_crawler.rs.

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:514-521: get_yarn_global_prefix_with runs yarn global dir with the inherited cwd and trusts its stdout.
  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1151: called unconditionally from get_global_node_modules_paths.
  • crates/socket-patch-core/src/utils/process.rs:137: SystemCommandRunner doesn't set a cwd.

Possible directions (for maintainers): run the probe from a neutral cwd such as the home or temp dir with no package.json above it, or check yarn --version first and skip the probe when the major version is 2 or higher (Berry has no global dir). Note that a .yarnrc.yml yarnPath in the cwd also makes any yarn invocation run a project-supplied JS file, so a neutral cwd covers both cases.

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