Skip to content

pnpm: version-pinned transitive update (pnpm update <pkg>@<version>) is a silent no-op -> security_update_not_possible #15766

Description

@esafak

Package ecosystem

pnpm (handled by the npm_and_yarn ecosystem)

Package manager version

11.17.0 (also reproduces on 11.5.2)

Language version

Node 22+

Manifest location and content

Any project whose pnpm-lock.yaml pins a transitive dependency at an older version that already satisfies its declaring parent's range, while a newer fixed version exists on the registry. In the real case: [email protected] declares brace-expansion: ^5.0.5 and the lockfile pins [email protected]; the advisory fix is 5.0.9.

Updated dependency

Transitive brace-expansion, pinned at 5.0.8 in pnpm-lock.yaml under [email protected]. Advisory: CVE-2026-69152 / GHSA-rgw5-rvv9-x895 (affected >= 4.0.0 < 5.0.9).

What you expected to see, versus what you actually saw

Expected: a security PR bumping brace-expansion 5.0.8 → 5.0.9 in pnpm-lock.yaml.

Actual: the security update job fails with security_update_not_possible. Relevant output from the failed update job (no conflicting dependencies):

The latest possible version of brace-expansion that can be installed is 5.0.8
The earliest fixed version is 5.0.9.

security_update_not_possible | {
  "dependency-name": "brace-expansion",
  "latest-resolvable-version": "5.0.8",
  "lowest-non-vulnerable-version": "5.0.9",
  "conflicting-dependencies": []
}

The job runs corepack pnpm update [email protected] --lockfile-only --no-save -r (the version-pinned form from SubdependencyVersionResolver#pnpm_update_command) and the lockfile is left unchanged.

Native package manager behavior

pnpm update [email protected] --lockfile-only --no-save -r changes nothing — the brace-expansion: 5.0.8 pin under [email protected] stays, even though 5.0.9 satisfies the declaring range ^5.0.5.

The unpinned form works:

pnpm update brace-expansion --lockfile-only --no-save -r → updates to 5.0.9.

Smallest manifest that reproduces the issue

mkdir repro && cd repro

# package.json
cat > package.json <<'EOF'
{
  "name": "repro",
  "version": "1.0.0",
  "private": true,
  "packageManager": "[email protected]",
  "dependencies": {
    "minimatch": "10.2.5"
  }
}
EOF

# 1. Simulate a lockfile generated before [email protected] existed,
#    by temporarily pinning 5.0.8 via an override.
printf 'overrides:\n  "brace-expansion": "5.0.8"\n' > pnpm-workspace.yaml
pnpm install --lockfile-only            # lockfile pins brace-expansion 5.0.8

# 2. Drop the override; the lockfile is now "stale" (5.0.9 is on the registry).
printf '' > pnpm-workspace.yaml

# 3. Dependabot-style update (version pinned) — silent NO-OP:
pnpm update [email protected] --lockfile-only --no-save -r
grep -c 'brace-expansion: 5.0.8' pnpm-lock.yaml    # -> 1 (unchanged)

# 4. Unpinned update — works:
pnpm install --lockfile-only                       # restore stale state
pnpm update brace-expansion --lockfile-only --no-save -r
grep -c 'brace-expansion: 5.0.8' pnpm-lock.yaml    # -> 0 (now 5.0.9)

The lockfile's dual-version state (5.0.8 under one parent, 5.0.9 under another) reproduces it as well — both versions satisfy their parents' ranges, so the pinned update has nothing to do.

Root cause hypothesis

For a transitive dependency, pnpm will not re-pin an entry away from a version that already satisfies the declaring parent's range, even when asked for an explicit higher in-range version (update <pkg>@<version>). The --depth Infinity / pnpm audit --fix fallbacks in SubdependencyVersionResolver that could recover are gated behind the enable-audit-fix-fallback experiment, which is not enabled by default → the resolver reports the locked 5.0.8 and the job ends in security_update_not_possible.

Related issues

Metadata

Metadata

Type

No type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions