Your bouncer is checking IDs, but someone found out that parentheses in the name make every fake ID look legitimate
CVE-2026-55487 is a normalization flaw in pnpm's allowBuilds security mechanism — the feature enterprises rely on to control which packages may execute lifecycle scripts during pnpm install. The generic peer-suffix normalizer was overly broad: it stripped parenthesized text not only from registry peer-dependency suffixes (its intended purpose) but also from opaque locators like git:, file:, URL, and tarball sources. This created identity collisions — if foo@https://host/pkg.tgz was approved in allowBuilds, then foo@https://host/pkg.tgz(evil) would normalize to the same value and pass the approval check. All pnpm versions prior to 10.34.2 and all 11.x versions prior to 11.5.3 are affected. Registry-published npm packages are NOT affected because symlinks are stripped during publish; the bug is limited to git, file, URL, and tarball dependencies.
The vendor's HIGH / 7.5 rating with AC:H and UI:R is directionally correct and already accounts for the chain complexity. However, the CVSS vector cannot capture two key real-world narrowings: (1) only non-registry dependency types trigger the bug, which represent a minority of dependencies in most projects, and (2) the target must have opted into allowBuilds — projects that haven't are already exposed to unrestricted lifecycle scripts by default, making this CVE moot for them. The vulnerability is serious in principle because it undermines a security control that security-conscious organizations specifically adopted, but the population of exploitable installations is meaningfully smaller than the pnpm user base at large. We downgrade modestly to HIGH / 7.0 — the supply-chain blast radius in CI/CD environments keeps it from falling to MEDIUM, but the narrow attack prerequisites lower the practical score.
5 steps from start to impact.
Attacker identifies an approved opaque source
package.json, pnpm-lock.yaml, or .pnpmfile.cjs to find a non-registry dependency (git, URL, tarball, or file source) that is explicitly approved in the project's allowBuilds configuration. Public repositories expose this information openly; private repos require insider access or a social engineering vector.- Target project uses pnpm with
allowBuildsenabled - Target project has at least one non-registry (opaque) dependency that is allow-listed
- Attacker has read access to the repository (public, or via fork/PR)
- Many enterprise projects use only registry dependencies, making opaque locators uncommon
- allowBuilds adoption is still ramping up — majority of pnpm users have not enabled it yet
Craft a colliding source locator
foo@https://host/pkg.tgz is approved, the attacker uses foo@https://host/pkg.tgz(evil-payload). The parenthesized text is stripped by the normalizer, producing an identity match.- Attacker controls a hosting endpoint (trivial — any git repo or HTTP server)
- Knowledge of the exact approved source string in the target project
- The collision must be byte-exact after normalization — random fuzzing won't work, the attacker needs the precise approved locator string
- Three specific collision forms exist (direct append, Rust parser misclass, source-only) — all require knowledge of the target config
Inject crafted dependency into the project
package.json or the lockfile, (b) compromising an upstream dependency that pulls in the malicious source, (c) a dependency confusion attack if the project resolves from multiple registries, or (d) social engineering a maintainer to accept the change.- A vector to modify
package.jsonorpnpm-lock.yamlin the target project - The change must survive code review or be injected into an automated pipeline
- Code review processes should catch unexpected dependency changes in
package.json - Lockfile diffs are large and noisy, but adding a new non-registry dependency is unusual enough to raise flags
- Branch protection and CODEOWNERS on lockfiles add friction
Victim runs pnpm install
pnpm install on the modified project. The pnpm resolver processes the crafted locator, the peer-suffix normalizer strips the parenthesized payload, and the allowBuilds check sees a match against the approved source. The malicious package's lifecycle scripts (preinstall, install, postinstall) are authorized to run.- Victim uses a vulnerable pnpm version (< 10.34.2 or 11.x < 11.5.3)
- The crafted dependency passes the normalized identity check
- pnpm's
onlyBuiltDependenciesandneverBuiltDependenciessettings may independently block the package - Some CI environments run with
--ignore-scriptsor--frozen-lockfilewhich prevents script execution or lockfile modification
pnpm install. EDR agents may flag script execution from node_modules/.pnpm/ paths.Arbitrary code execution
- Lifecycle scripts are not sandboxed (default pnpm behavior)
- The execution context has valuable credentials or publish permissions
- CI runners with minimal IAM roles and ephemeral credentials limit lateral movement
- Workstations with credential managers requiring biometric unlock reduce secret theft
- Network segmentation may prevent C2 callbacks from build environments
pnpm install spawning unexpected network connections or writing to sensitive paths. Cloud audit logs (AWS CloudTrail, GCP Audit) capture any API calls made with stolen credentials.corepack prepare or CI image rebuilds.--ignore-scripts to pnpm install in CI prevents ALL lifecycle script execution, completely breaking the attack path at Step 4. This is appropriate for build-only CI stages that don't need native module compilation. --frozen-lockfile independently prevents lockfile tampering. Apply as an interim mitigation within 30 days per noisgate mitigation SLA.package.json, pnpm-lock.yaml, and .pnpmfile.cjs. This adds a human gate at Step 3 — the injection step — making it significantly harder for an attacker to land a crafted dependency via PR. Most enterprises already have this; verify it's enforced across all repositories.allowBuilds configuration across all repositories. If you have approved opaque locators (git, URL, tarball, file sources), those are the specific entries an attacker would target for collision. Consider whether those dependencies can be migrated to registry-published packages, which are immune to this CVE. Document the business justification for any remaining opaque approvals.- WAF / network firewall — pnpm is a local tool and CI/CD runner process. There is no network request to inspect or block; the vulnerability is in local lockfile/manifest processing logic.
- npm audit / pnpm audit — These tools check for known vulnerabilities in *dependencies*, not in the package manager itself. They will not flag that your pnpm binary is vulnerable to CVE-2026-55487.
- Pinning to registry-only dependencies — While registry packages are immune, you cannot always eliminate git/file deps (internal packages, monorepo links, pre-release forks). This is aspirational, not a compensating control.
- Container image scanning (Trivy, Grype) — Most scanners detect OS-level and language-level library CVEs but do not inventory the pnpm binary version or flag CVE-2026-55487 against it. Verify your scanner's coverage before relying on it.
The supporting signals.
| In-the-Wild Exploitation | No evidence. Not listed in CISA KEV. No campaigns, advisories, or incident reports reference active exploitation of this CVE as of 2026-10-02. |
|---|---|
| Proof-of-Concept Availability | No public PoC. Checked pocindex.io, GitHub, ExploitDB — no weaponized exploit or reproduction repo found. The GHSA advisory describes three collision vectors in detail, which constitutes a conceptual PoC but not a runnable exploit. |
| EPSS Score | 0.181% (7th percentile) — places this CVE well below the exploitation-likely threshold. Consistent with no known in-the-wild activity. |
| KEV Status | Not listed. No CISA Known Exploited Vulnerability entry as of 2026-10-02. |
| CVSS Vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H — Network-reachable but High complexity and User interaction required (victim must run pnpm install). All three impact metrics are High because successful exploitation yields full code execution. |
| Affected Versions | pnpm < 10.34.2 (entire 0.x–10.x line) and pnpm ≥ 11.0.0, < 11.5.3. Only non-registry dependencies (git, file, URL, tarball) are affected; standard npm registry packages are not vulnerable. |
| Fixed Versions | 10.34.2 (10.34.2" target="_blank" rel="noopener">commit 14bceb1) and 11.5.3 (11.5.3" target="_blank" rel="noopener">commit bf1b731). Fix enforces byte-exact matching for opaque dependency identities. |
| Installed Base / Exposure | pnpm has ~20% market share among JavaScript developers (2024 State of Frontend) with 216M+ weekly npm downloads (Sept 2026). Predominantly deployed on developer workstations and CI/CD runners — not internet-facing services. Shodan/GreyNoise/Censys are not applicable (no listening service). |
| Disclosure Timeline | Fixed in pnpm 10.34.2 (2026-06-10) and 11.5.3 (2026-06-12). GHSA published 2026-06-12. CVE published 2026-06-25. Credited to zkochan (Zoltan Kochan, pnpm maintainer). |
| Related Advisories | The same release (10.34.2) also fixed GHSA-3qhv-2rgh-x77r (env-var expansion in repo-controlled .npmrc). Both are supply-chain hardening fixes — review together. |
Sources.
Why this verdict
- Friction: allowBuilds is opt-in. The vulnerability only affects projects that have explicitly enabled
allowBuilds. Projects without it are already exposed to unrestricted lifecycle scripts by default — this CVE adds zero incremental risk to them. This dramatically narrows the exploitable population beyond what the CVSS vector captures. - Friction: non-registry dependencies only. Standard npm registry packages are unaffected. The bug requires git, file, URL, or tarball dependencies — a minority of the dependency graph in most projects. The attacker must also know the exact approved source string to create a collision.
- Friction: multi-step injection with human gates. The attacker must get a crafted dependency into the target's
package.jsonor lockfile, which typically requires surviving code review, passing branch protection checks, or first compromising an upstream dependency. TheAC:Hin the CVSS vector partially captures this but understates the practical difficulty in enterprises with PR review policies. - Role multiplier: CI/CD pipelines (HIGH-value role). pnpm is canonically deployed in CI/CD — GitHub Actions, GitLab CI, Jenkins — where lifecycle script execution has supply-chain-scale blast radius. A successful exploit in CI can inject backdoors into published artifacts, steal deployment secrets, and pivot to production infrastructure. The chain DOES succeed in this role (CI runners execute
pnpm installroutinely, often without--ignore-scripts). This supply-chain impact floors the verdict at HIGH despite the narrow attack prerequisites. - Role multiplier: developer workstations (MEDIUM-value role). On developer machines, the impact is host-level: source code theft, credential harvesting, SSH key exfiltration. Serious but not fleet-scale. EDR and endpoint controls provide partial mitigation.
- No exploitation signal. Zero in-the-wild exploitation, no public PoC, EPSS at 7th percentile, not in KEV. The theoretical severity is real but the threat is not yet actualized. This supports a score at the lower end of HIGH rather than the upper end.
Why not higher?
The vulnerability lacks any exploitation in the wild, has no public weaponized PoC, and carries an EPSS in the 7th percentile. The attack requires a multi-step chain with human gates (code review, branch protection) and only works against the subset of pnpm users who have opted into allowBuilds AND use non-registry dependencies — a minority of the already-minority pnpm user base. Upgrading to CRITICAL would require either active exploitation or a much broader attack surface.
Why not lower?
pnpm is canonically a CI/CD infrastructure component with ~20% JS developer market share and 216M+ weekly downloads. When the chain succeeds in a CI runner, the blast radius is supply-chain-scale: artifact tampering, secret exfiltration, production pivot. The vulnerability specifically undermines allowBuilds, which is the security control enterprises adopted to *prevent* exactly this class of attack — defeating a security mechanism warrants higher severity than a novel attack vector. The floor for supply-chain-capable code execution in CI/CD tooling is HIGH.
Crowdsourced verification payload.
Run this script on any host where pnpm is installed — developer workstations, CI runner images, or build servers. Invoke with bash check_cve_2026_55487.sh. No special privileges required; it only reads the pnpm version string.
#!/usr/bin/env bash
# CVE-2026-55487 checker — pnpm allowBuilds manifest identity spoof
# VULNERABLE: pnpm < 10.34.2 or 11.0.0 <= pnpm < 11.5.3
# PATCHED: pnpm >= 10.34.2 (10.x) or pnpm >= 11.5.3 (11.x+)
# Exit codes: 1 = VULNERABLE, 0 = PATCHED, 2 = UNKNOWN
set -euo pipefail
CVE="CVE-2026-55487"
if ! command -v pnpm &>/dev/null; then
echo "UNKNOWN - pnpm is not installed or not in PATH ($CVE)"
exit 2
fi
VERSION=$(pnpm --version 2>/dev/null || true)
if [[ -z "$VERSION" ]]; then
echo "UNKNOWN - could not determine pnpm version ($CVE)"
exit 2
fi
# Parse major.minor.patch (strip any pre-release/build suffix)
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
PATCH="${PATCH%%-*}"
PATCH="${PATCH%%+*}"
# Ensure numeric
if ! [[ "$MAJOR" =~ ^[0-9]+$ && "$MINOR" =~ ^[0-9]+$ && "$PATCH" =~ ^[0-9]+$ ]]; then
echo "UNKNOWN - could not parse pnpm version '$VERSION' ($CVE)"
exit 2
fi
echo "Detected pnpm $VERSION (parsed: $MAJOR.$MINOR.$PATCH)"
vulnerable() {
echo "VULNERABLE - pnpm $VERSION is affected by $CVE (upgrade to >=10.34.2 or >=11.5.3)"
exit 1
}
patched() {
echo "PATCHED - pnpm $VERSION is not affected by $CVE"
exit 0
}
if (( MAJOR < 10 )); then
vulnerable
elif (( MAJOR == 10 )); then
if (( MINOR < 34 )); then
vulnerable
elif (( MINOR == 34 && PATCH < 2 )); then
vulnerable
else
patched
fi
elif (( MAJOR == 11 )); then
if (( MINOR < 5 )); then
vulnerable
elif (( MINOR == 5 && PATCH < 3 )); then
vulnerable
else
patched
fi
else
# Major > 11 — assumed patched
patched
fi