VYPR
High severityOSV Advisory· Published Jan 7, 2026· Updated Feb 26, 2026

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

CVE-2025-69264

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.

PackageAffected versionsPatched versions
pnpmnpm
>= 10.0.0, < 10.26.010.26.0

Affected products

1
  • Range: v1, v10.0.0, v10.1.0, …

Patches

1
73cc63504d9b

feat: support blockExoticSubdeps option to disallow non-trusted dep sources in subdeps (#10265)

https://github.com/pnpm/pnpmOrenDec 10, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.