[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 |
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.
[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 toyarn global dirin the current working directory (crates/socket-patch-core/src/crawlers/npm_crawler.rs:517). Yarn Berry (2, 3 and 4) has noglobalcommand. When the cwd is inside a Berry project, Berry treats an unknown command asyarn run <name>, soyarn global dirruns the project's"global"script frompackage.jsonwith the argumentdir. socket-patch then takes whatever that script prints on stdout, appends/node_modules, and scans that directory as a global install.Impact
socket-patch scan -g(orSOCKET_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 runor lifecycle step is involved.--mode agent/--apply, socket-patch patches files in it. This breaks the rule that-gscans only global locations."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)On the sandbox this reproduced on every run (well over 2):
scan -greported 552 packages with the script and 551 without, and createdMARKEReach time. It also happens under the PnP linker, and withSOCKET_GLOBAL=1.--global-prefixisn't affected, because it skips discovery.Expected vs actual
--globaloperates on globally installed packages (--help: "Operate on globally-installed packages"). docs/usage.md and the CLI contract describe-gas targeting the machine tree, not the project. A global scan should read install locations and never run project-defined scripts.globalscript 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)The probe printed lines like
RESULT os=macos-latest yarn=4.12.0 script_runs=1 scanned_with_script=652 scanned_without=651. On Windowsscript_runs=0:yarnthere is an npm.cmdshim, andstd::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. Theyarn global dirshell-out predates the current history ofnpm_crawler.rs.Suspect code
crates/socket-patch-core/src/crawlers/npm_crawler.rs:514-521:get_yarn_global_prefix_withrunsyarn global dirwith the inherited cwd and trusts its stdout.crates/socket-patch-core/src/crawlers/npm_crawler.rs:1151: called unconditionally fromget_global_node_modules_paths.crates/socket-patch-core/src/utils/process.rs:137:SystemCommandRunnerdoesn'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.jsonabove it, or checkyarn --versionfirst and skip the probe when the major version is 2 or higher (Berry has no global dir). Note that a.yarnrc.ymlyarnPathin the cwd also makes anyyarninvocation run a project-supplied JS file, so a neutral cwd covers both cases.