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
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) |
noisgate verdict.
The single most decisive factor is that exploitation requires an attacker to land a malicious .patch file in a repo AND a victim to run pnpm install against that branch — closed enterprise monorepos with PR review almost never satisfy both. Blast radius is also bounded to the install user's UID, not host-wide privilege escalation.
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.
What to do — in priority order.
- Pin pnpm to
10.34.0/11.4.0or later via Corepack/packageManagerfield — Single-line fix — set"packageManager": "[email protected]"(or 11.4.0+) in everypackage.jsonand 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. - Block CI auto-install on untrusted PR branches — Gate
pnpm installbehindpull_request_targetreview approvals (GitHub Actions) or equivalent in GitLab/Jenkins. The vuln is dead if untrusted code never runspnpm installuntil a human approves. Implement within 2 weeks. - Audit repos for
patchedDependenciesand lock down review on.patchfile changes — Add CODEOWNERS rules requiring senior maintainer review for any*.patchfile diff and anypatchedDependenciesmodification. Most repos don't use this feature — your search will be short. - Run CI under rootless/unprivileged ephemeral runners — Containerized ephemeral runners with read-only HOME and no long-lived secrets mounted neuter the traversal write target. Standard supply-chain hygiene.
- Rotate any npm registry tokens potentially exposed to a vulnerable pnpm run — If you cannot quickly determine which pre-
10.34.0installs 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.
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
If you remember one thing.
10.34.0 or in the 11.0–11.3 range, and grep repos for patchedDependencies — that's your real exposed surface. Per the noisgate mitigation SLA, MEDIUM has no mitigation deadline, so go straight to the noisgate remediation SLA of ≤ 365 days to upgrade pnpm via the packageManager field or Corepack — but if any repo accepts untrusted PRs and auto-runs pnpm install in CI, treat that subset as HIGH-equivalent and pull mitigation (block auto-install on PR branches) to ≤ 30 days, remediation to ≤ 180 days. Not KEV-listed, EPSS 0.0025, no observed exploitation — no fire drill, but don't let it rot in the backlog if you ship JS at scale.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.