pnpm v10+ Bypass "Dependency lifecycle scripts execution disabled by default"
Description
pnpm is a package manager. Versions 10.0.0 through 10.25 allow 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. This issue is fixed in version 10.26.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pnpmnpm | >= 10.0.0, < 10.26.0 | 10.26.0 |
Affected products
1Patches
173cc63504d9bfeat: support blockExoticSubdeps option to disallow non-trusted dep sources in subdeps (#10265)
9 files changed · +112 −0
.changeset/better-parents-tell.md+16 −0 added@@ -0,0 +1,16 @@ +--- +"@pnpm/resolve-dependencies": minor +"@pnpm/core": minor +"@pnpm/config": minor +"pnpm": minor +--- + +Added a new setting `blockExoticSubdeps` that prevents the resolution of exotic protocols in transitive dependencies. + +When set to `true`, direct dependencies (those listed in your root `package.json`) may still use exotic sources, but all transitive dependencies must be resolved from a trusted source. Trusted sources include the configured registry, local file paths, workspace links, trusted GitHub repositories (node, bun, deno), and custom resolvers. + +This helps to secure the dependency supply chain. Packages from trusted sources are considered safer, as they are typically subject to more reliable verification and scanning for malware and vulnerabilities. + +**Exotic sources** are dependency locations that bypass the usual trusted resolution process. These protocols are specifically targeted and blocked: Git repositories (`git+ssh://...`) and direct URL links to tarballs (`https://.../package.tgz`). + +Related PR: [#10265](https://github.com/pnpm/pnpm/pull/10265).
config/config/src/Config.ts+1 −0 modified@@ -190,6 +190,7 @@ export interface Config extends OptionsFromRootManifest { ignoreWorkspaceCycles?: boolean disallowWorkspaceCycles?: boolean packGzipLevel?: number + blockExoticSubdeps?: boolean registries: Registries sslConfigs: Record<string, SslConfig>
config/config/src/index.ts+1 −0 modified@@ -181,6 +181,7 @@ export async function getConfig (opts: { 'public-hoist-pattern': [], 'recursive-install': true, registry: npmDefaults.registry, + 'block-exotic-subdeps': false, 'resolution-mode': 'highest', 'resolve-peers-from-workspace-root': true, 'save-peer': false,
config/config/src/types.ts+1 −0 modified@@ -91,6 +91,7 @@ export const types = Object.assign({ 'public-hoist-pattern': Array, 'publish-branch': String, 'recursive-install': Boolean, + 'block-exotic-subdeps': Boolean, reporter: String, 'resolution-mode': ['highest', 'time-based', 'lowest-direct'], 'resolve-peers-from-workspace-root': Boolean,
pkg-manager/core/src/install/extendInstallOptions.ts+2 −0 modified@@ -170,6 +170,7 @@ export interface StrictInstallOptions { minimumReleaseAgeExclude?: string[] trustPolicy?: TrustPolicy trustPolicyExclude?: string[] + blockExoticSubdeps?: boolean } export type InstallOptions = @@ -269,6 +270,7 @@ const defaults = (opts: InstallOptions): StrictInstallOptions => { excludeLinksFromLockfile: false, virtualStoreDirMaxLength: 120, peersSuffixMaxLength: 1000, + blockExoticSubdeps: false, } as StrictInstallOptions }
pkg-manager/core/src/install/index.ts+1 −0 modified@@ -1228,6 +1228,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => { minimumReleaseAgeExclude: opts.minimumReleaseAgeExclude, trustPolicy: opts.trustPolicy, trustPolicyExclude: opts.trustPolicyExclude, + blockExoticSubdeps: opts.blockExoticSubdeps, } ) if (!opts.include.optionalDependencies || !opts.include.devDependencies || !opts.include.dependencies) {
pkg-manager/core/test/install/blockExoticSubdeps.ts+57 −0 added@@ -0,0 +1,57 @@ +import { prepareEmpty } from '@pnpm/prepare' +import { addDependenciesToPackage } from '@pnpm/core' +import { testDefaults } from '../utils/index.js' + +test('blockExoticSubdeps disallows git dependencies in subdependencies', async () => { + prepareEmpty() + + await expect(addDependenciesToPackage({}, + // @pnpm.e2e/has-aliased-git-dependency has a git-hosted subdependency (say-hi from github:zkochan/hi) + ['@pnpm.e2e/has-aliased-git-dependency'], + testDefaults({ blockExoticSubdeps: true, fastUnpack: false }) + )).rejects.toThrow('is not allowed in subdependencies when blockExoticSubdeps is enabled') +}) + +test('blockExoticSubdeps allows git dependencies in direct dependencies', async () => { + const project = prepareEmpty() + + // Direct git dependency should be allowed even when blockExoticSubdeps is enabled + const { updatedManifest: manifest } = await addDependenciesToPackage( + {}, + ['kevva/is-negative#1.0.0'], + testDefaults({ blockExoticSubdeps: true }) + ) + + project.has('is-negative') + + expect(manifest.dependencies).toStrictEqual({ + 'is-negative': 'github:kevva/is-negative#1.0.0', + }) +}) + +test('blockExoticSubdeps allows registry dependencies in subdependencies', async () => { + const project = prepareEmpty() + + // A package with only registry subdependencies should work fine + await addDependenciesToPackage( + {}, + ['is-positive@1.0.0'], + testDefaults({ blockExoticSubdeps: true }) + ) + + project.has('is-positive') +}) + +test('blockExoticSubdeps: false (default) allows git dependencies in subdependencies', async () => { + const project = prepareEmpty() + + // Without blockExoticSubdeps (or with it set to false), git subdeps should be allowed + await addDependenciesToPackage( + {}, + ['@pnpm.e2e/has-aliased-git-dependency'], + testDefaults({ blockExoticSubdeps: false, fastUnpack: false }) + ) + + const m = project.requireModule('@pnpm.e2e/has-aliased-git-dependency') + expect(m).toBe('Hi') +})
pkg-manager/resolve-dependencies/src/resolveDependencies.ts+31 −0 modified@@ -187,6 +187,7 @@ export interface ResolutionContext { publishedByExclude?: PackageVersionPolicy trustPolicy?: TrustPolicy trustPolicyExclude?: PackageVersionPolicy + blockExoticSubdeps?: boolean } export interface MissingPeerInfo { @@ -1387,6 +1388,21 @@ async function resolveDependency ( }, }) + // Check if exotic dependencies are disallowed in subdependencies + if ( + ctx.blockExoticSubdeps && + options.currentDepth > 0 && + !isNonExoticDep(pkgResponse.body.resolvedVia) + ) { + const error = new PnpmError( + 'EXOTIC_SUBDEP', + `Exotic dependency "${wantedDependency.alias ?? wantedDependency.bareSpecifier}" (resolved via ${pkgResponse.body.resolvedVia}) is not allowed in subdependencies when blockExoticSubdeps is enabled` + ) + error.prefix = options.prefix + error.pkgsStack = getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById) + throw error + } + if (ctx.allPreferredVersions && pkgResponse.body.manifest?.version) { if (!ctx.allPreferredVersions[pkgResponse.body.manifest.name]) { ctx.allPreferredVersions[pkgResponse.body.manifest.name] = {} @@ -1781,3 +1797,18 @@ function getCatalogExistingVersionFromSnapshot ( ? existingCatalogResolution.version : undefined } + +const NON_EXOTIC_RESOLVED_VIA = new Set([ + 'custom-resolver', + 'github.com/denoland/deno', + 'github.com/oven-sh/bun', + 'jsr-registry', + 'local-filesystem', + 'nodejs.org', + 'npm-registry', + 'workspace', +]) + +function isNonExoticDep (resolvedVia: string | undefined): boolean { + return resolvedVia != null && NON_EXOTIC_RESOLVED_VIA.has(resolvedVia) +}
pkg-manager/resolve-dependencies/src/resolveDependencyTree.ts+2 −0 modified@@ -143,6 +143,7 @@ export interface ResolveDependenciesOptions { minimumReleaseAgeExclude?: string[] trustPolicy?: TrustPolicy trustPolicyExclude?: string[] + blockExoticSubdeps?: boolean } export interface ResolveDependencyTreeResult { @@ -208,6 +209,7 @@ export async function resolveDependencyTree<T> ( publishedByExclude: opts.minimumReleaseAgeExclude ? createPackageVersionPolicyByExclude(opts.minimumReleaseAgeExclude, 'minimumReleaseAgeExclude') : undefined, trustPolicy: opts.trustPolicy, trustPolicyExclude: opts.trustPolicyExclude ? createPackageVersionPolicyByExclude(opts.trustPolicyExclude, 'trustPolicyExclude') : undefined, + blockExoticSubdeps: opts.blockExoticSubdeps, } function createPackageVersionPolicyByExclude (patterns: string[], key: string): PackageVersionPolicy {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-379q-355j-w6rjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-69264ghsaADVISORY
- github.com/pnpm/pnpm/commit/73cc63504d9bc360c43e4b2feb9080677f03c5b5ghsax_refsource_MISCWEB
- github.com/pnpm/pnpm/security/advisories/GHSA-379q-355j-w6rjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.