A malicious pull request smuggles `../../` in a .patch header and pnpm writes wherever the developer's shell can reach
pnpm's patch application pipeline (@pnpm/patch-package) parses .patch files referenced from patchedDependencies in package.json and applies them during pnpm install. The parser pulls target paths straight out of diff --git a/... b/... headers with no normalization, so an attacker who can land a crafted .patch file in a repo gets arbitrary file write/delete as the user running install — overwriting ~/.ssh/authorized_keys, ~/.bashrc, CI runner config, or anything else under that UID. Affected: pnpm < 10.34.0 and 11.0.0 ≤ pnpm < 11.4.0. Fixed in 10.34.0 and 11.4.0 (GHSA-rxhj-4m44-96r4).
Vendor pegged this HIGH (7.3) with PR:L/UI:R, which is fair on paper but slightly overstated for a typical enterprise dev fleet. The vuln requires an attacker to land a malicious patch file *and* a maintainer to run pnpm install against the poisoned branch — UI:R is doing real work here. The CVSS does not capture that the realistic victim population is open-source repos that auto-install untrusted PR branches in CI, not closed corporate monorepos where every patch file goes through code review. We land at MEDIUM for the enterprise patching audience.
4 steps from start to impact.
Land a malicious .patch file in target repo
.patch file whose diff --git header references a traversal path such as a/../../../../home/dev/.ssh/authorized_keys. The package.json patchedDependencies block points pnpm at this patch. Patch-file diffs are notoriously opaque in review — reviewers eyeball the hunk body, not the header path.- Write access to a fork or PR branch the victim will check out
- Repo uses
patchedDependencies
- Closed monorepos do not accept third-party PRs
- Mature review processes flag any change to
patchedDependenciesor new.patchfiles
Victim checks out branch and runs pnpm install
pnpm install. pnpm reads patchedDependencies, locates the .patch, and feeds it to @pnpm/patch-package for application against the resolved package directory under node_modules/.pnpm/<pkg>.- pnpm version
< 10.34.0or11.0.0 ≤ v < 11.4.0 - Install triggered against attacker's branch
- Modern CI runs untrusted PRs in ephemeral isolated containers — blast radius dies with the container
- Pre-merge review-then-CI gating means no install before human approval
node_modules, which is anomalous and detectable.Traversal escapes node_modules and writes attacker payload
../../ segments resolve outside the package dir. Attacker writes to ~/.ssh/authorized_keys, ~/.bashrc, ~/.npmrc (steal registry tokens), or in CI: /github/home/.gitconfig, secret files, or the runner's job script.- No filesystem ACL preventing the install user from writing the target path
- Install user owns the targeted directory
- Rootless / unprivileged CI runners limit reachable files
- Hardened dev images with read-only HOME are unaffected
~/.ssh, ~/.bashrc, and runner workspace flags writes from non-shell parent processes.Persistence or credential theft
~/.npmrc, SSH keys, AWS creds) on a next CI run, or backdoors developer shell via ~/.bashrc for code-exec on next terminal launch. From a developer laptop this becomes a foothold; from a CI runner it becomes lateral movement into the build pipeline.- Stolen tokens have not been short-lived OIDC
- Compromised host has outbound network egress
- Short-lived OIDC tokens (GitHub Actions, GitLab CI) expire with the job
- Egress filtering on CI runners blocks exfil
10.34.0 / 11.4.0 or later via Corepack/packageManager field — Single-line fix — set "packageManager": "[email protected]" (or 11.4.0+) in every package.json and enable Corepack on CI. With MEDIUM verdict and no mitigation SLA, fold this into the next sprint and complete remediation within the noisgate 365-day window — but if you accept untrusted PRs anywhere, pull it forward to 30 days.pnpm install behind pull_request_target review approvals (GitHub Actions) or equivalent in GitLab/Jenkins. The vuln is dead if untrusted code never runs pnpm install until a human approves. Implement within 2 weeks.patchedDependencies and lock down review on .patch file changes — Add CODEOWNERS rules requiring senior maintainer review for any *.patch file diff and any patchedDependencies modification. Most repos don't use this feature — your search will be short.10.34.0 installs ran against PR branches, rotate npm publishing tokens proactively. Cheap insurance.- Network egress filtering — the write happens locally on disk; no outbound traffic needed for the file write itself.
- WAF / IDS at the edge — the vuln triggers at
pnpm installtime, not over HTTP. - Generic Dependabot/Renovate updates of npm package dependencies — they bump
dependencies, not the pnpm binary itself; you needpackageManagerfield or Corepack updates. - SAST scanning the application source — the bug is in pnpm's parser, not your code.
The supporting signals.
| In the wild | No public exploitation reported as of 2026-07-01. No campaign attribution. |
|---|---|
| PoC availability | Proof-of-concept demonstrated by reporter AutoFyn in the GHSA writeup; no weaponized public exploit on GitHub at time of assessment. |
| EPSS | 0.0025 (~0.25%) — bottom-quartile, FIRST predicts very low near-term exploitation probability. |
| KEV status | Not listed. No CISA KEV entry. |
| CVSS vector | AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:H — network reachable but requires low-priv attacker (PR contributor) and victim interaction (pnpm install). |
| Affected versions | pnpm < 10.34.0 and 11.0.0 ≤ v < 11.4.0 |
| Fixed versions | pnpm 10.34.0, 11.4.0 and later. No distro backports — pnpm ships via npm/Corepack. |
| Exposure data | pnpm has ~20% of npm-ecosystem mindshare per State of JS surveys; affected subset is repos using patchedDependencies AND accepting untrusted PRs — a small fraction. |
| Disclosed | 2026-06-25, coordinated via GitHub Security Advisory GHSA-rxhj-4m44-96r4 |
| Reporter | AutoFyn (automated dependency audit research) |
Sources.
Why this verdict
- Attacker-position prerequisite: requires the attacker to either be a PR contributor or already own a branch the victim checks out — this is post-social-engineering or post-supply-chain access, not unauthenticated remote.
- User interaction gate:
UI:Ris real — the victim must explicitly runpnpm installagainst the poisoned branch. CI that doesn't auto-install untrusted PR branches is unaffected. - Blast radius cap: writes execute as the install user. No SYSTEM/root escalation, no kernel impact, no fleet-wide pivot from a single host unless secrets harvested from
~/.npmrcchain into the registry. - Role multiplier — developer workstation: chain succeeds and yields workstation-local code exec on next shell. Blast radius: one developer host, recoverable.
- Role multiplier — shared CI runner (self-hosted Jenkins, GitHub Actions self-hosted): chain succeeds and can write the runner workspace / steal long-lived registry tokens. Blast radius: the build pipeline. This is the worst plausible outcome and pushes us above LOW but does not reach the canonical supply-chain-registry floor — pnpm itself is not the registry, just the consumer.
- Friction stack: narrow
patchedDependenciesuser base + needs untrusted PR ingestion +UI:R+ EPSS 0.0025 + no KEV + no observed exploitation = downgrade from vendor HIGH to MEDIUM.
Why not higher?
Not HIGH because the prerequisite chain (attacker-contributed patch file + victim-initiated install + writable HOME) is unmet in the typical closed-source enterprise repo. There is no unauthenticated remote vector, no automatic exploitation, EPSS is 0.0025, and KEV has not picked it up. A floor to HIGH would require canonical deployment in a high-value role — pnpm is a dev-time tool, not an identity/PKI/hypervisor component.
Why not lower?
Not LOW because the impact when the chain DOES succeed is real arbitrary file write at user UID — including ~/.ssh/authorized_keys, ~/.npmrc (npm token theft → potential downstream registry pivot), and CI runner workspace files. Open-source maintainers and shops that auto-install untrusted PR branches in CI face a genuine, non-trivial risk that warrants tracked remediation.
Crowdsourced verification payload.
Run on each developer workstation and CI runner that uses pnpm. Invoke as bash check-cve-2026-50015.sh. No special privileges required — just executes pnpm --version and corepack info. Also scans the current repo for patchedDependencies, which scopes the exposure.
#!/usr/bin/env bash
# noisgate verification — CVE-2026-50015 (pnpm patch-file path traversal)
# Exit codes: 0 PATCHED, 1 VULNERABLE, 2 UNKNOWN
set -u
VERDICT="UNKNOWN"
REASON=""
if ! command -v pnpm >/dev/null 2>&1; then
echo "PATCHED (no pnpm installed)"
exit 0
fi
RAW=$(pnpm --version 2>/dev/null | tr -d '[:space:]')
if [[ -z "$RAW" ]]; then
echo "UNKNOWN (could not read pnpm --version)"
exit 2
fi
# Strip prerelease suffix
VER="${RAW%%-*}"
IFS='.' read -r MAJ MIN PATCH <<<"$VER"
MAJ=${MAJ:-0}; MIN=${MIN:-0}; PATCH=${PATCH:-0}
is_vulnerable() {
# < 10.34.0 -> vulnerable
if (( MAJ < 10 )); then return 0; fi
if (( MAJ == 10 )); then
if (( MIN < 34 )); then return 0; fi
return 1
fi
# 11.x: vulnerable when < 11.4.0
if (( MAJ == 11 )); then
if (( MIN < 4 )); then return 0; fi
return 1
fi
# 12+ assumed patched
return 1
}
if is_vulnerable; then
VERDICT="VULNERABLE"
REASON="pnpm $RAW is in affected range (<10.34.0 or 11.0.0 \u2264 v < 11.4.0)"
else
VERDICT="PATCHED"
REASON="pnpm $RAW is at or above fixed release"
fi
# Bonus: scope check — does current dir use patchedDependencies?
if [[ -f package.json ]]; then
if grep -q '"patchedDependencies"' package.json 2>/dev/null; then
REASON="$REASON | package.json declares patchedDependencies — high relevance"
fi
fi
echo "$VERDICT: $REASON"
case "$VERDICT" in
VULNERABLE) exit 1 ;;
PATCHED) exit 0 ;;
*) exit 2 ;;
esac