VDB
Sign up
HIGH8.8

GHSA-379q-355j-w6rj

pnpm v10+ Bypass "Dependency lifecycle scripts execution disabled by default"

Quick fix

GHSA-379q-355j-w6rj — pnpm: upgrade to the fixed version with the command below.

npm install pnpm@10.26.0

Details

# pnpm v10+ Git Dependency Script Execution Bypass

### Summary

A security bypass vulnerability in pnpm v10+ allows git-hosted dependencies to execute arbitrary code during `pnpm install`, circumventing the v10 security feature "Dependency lifecycle scripts execution disabled by default". While pnpm v10 blocks `postinstall` scripts via the `onlyBuiltDependencies` mechanism, git dependencies can still execute `prepare`, `prepublish`, and `prepack` scripts during the fetch phase, enabling remote code execution without user consent or approval.

### Details

pnpm v10 introduced a security feature to disable dependency lifecycle scripts by default ([PR #8897](https://github.com/pnpm/pnpm/pull/8897)). This is implemented by setting `onlyBuiltDependencies = []` when no build policy is configured:

**File:** `pkg-manager/core/src/install/extendInstallOptions.ts` (lines 290-291) ```typescript if (opts.neverBuiltDependencies == null && opts.onlyBuiltDependencies == null && opts.onlyBuiltDependenciesFile == null) { opts.onlyBuiltDependencies = [] } ```

This creates an allowlist that blocks all packages from running scripts during the **BUILD phase** in `exec/build-modules/src/index.ts`.

However, git-hosted dependencies are processed differently. During the **FETCH phase**, git packages are prepared using `preparePackage()`:

**File:** `exec/prepare-package/src/index.ts` (lines 28-57) ```typescript export async function preparePackage (opts: PreparePackageOptions, gitRootDir: string, subDir: string) { // ... if (opts.ignoreScripts) return { shouldBeBuilt: true, pkgDir } // Only checks ignoreScripts, not onlyBuiltDependencies

const execOpts: RunLifecycleHookOptions = { // ... rawConfig: omit(['ignore-scripts'], opts.rawConfig), // Explicitly removes ignore-scripts! }

// Runs npm/pnpm install await runLifecycleHook(installScriptName, manifest, execOpts)

// Runs prepare scripts for (const scriptName of PREPUBLISH_SCRIPTS) { // ['prepublish', 'prepack', 'publish'] await runLifecycleHook(newScriptName, manifest, execOpts) } } ```

The `ignoreScripts` option defaults to `false` and is completely separate from `onlyBuiltDependencies`. The `onlyBuiltDependencies` allowlist is never consulted during the fetch phase.

**Affected scripts that execute during fetch:** - `prepare` - `prepublish` - `prepack`

**Attack vectors:** - `git+https://github.com/attacker/malicious.git` - `github:attacker/malicious` - `gitlab:attacker/malicious` - `bitbucket:attacker/malicious` - `git+ssh://git@github.com/attacker/malicious.git` - `git+file:///path/to/local/repo`

### PoC

**Prerequisites:** - pnpm v10.0.0 or later (tested on v10.23.0 and v11.0.0-alpha.1) - git

**Steps to reproduce:**

1. Extract the attached [poc.zip](https://github.com/user-attachments/files/23797816/poc.zip)

2. Run the PoC script: ```bash cd poc chmod +x run-poc.sh ./run-poc.sh ```

3. Verify the marker file was created by the malicious script: ```bash cat /tmp/pnpm-vuln-poc-marker.txt ```

**Manual reproduction:**

1. Create a malicious package with a `prepare` script: ```json { "name": "malicious-pkg", "version": "1.0.0", "scripts": { "prepare": "node -e \"require('fs').writeFileSync('/tmp/pwned.txt', 'RCE!')\"" } } ```

2. Initialize it as a git repo and commit the files

3. Create a victim project that depends on it (just have to make sure it actually git clones and not just downloads a tarball): ```json { "dependencies": { "malicious-pkg": "git+file:///path/to/malicious-pkg" } } ```

4. Run `pnpm install` - the prepare script executes without any warning or approval prompt

### Impact

**Severity: High**

**Who is impacted:** - All pnpm v10+ users - Users who believed they were protected by the v10 "scripts disabled by default" feature - CI/CD pipelines

**Attack scenarios:** 1. **Supply chain attack:** An attacker compromises a dependency, adding to it a malicious git dependency that executes arbitrary code during `pnpm install`

**What an attacker can do:** - Execute arbitrary code with the victim's privileges - Exfiltrate environment variables, secrets, and credentials - Modify source code or inject backdoors - Establish persistence or reverse shells - Access the filesystem and network

**Why this bypasses security expectations:** - pnpm v10 changelog explicitly states "Lifecycle scripts of dependencies are not executed during installation by default" - Users expect git dependencies to follow the same security model as npm registry packages - There is no warning that git dependencies are treated differently - The `onlyBuiltDependencies` configuration does not affect git dependencies

Are you affected?

Enter the version of the package you're using.

Affected packages

npm/pnpm
Introduced in: 10.0.0Fixed in: 10.26.0
Fixnpm install pnpm@10.26.0

References