UNKNOWN npm

pnpm has Path Traversal via arbitrary file permission modification

GHSA-v253-rj99-jwpq · CVE-2026-24131

Published · Modified

AI SAST

Find this class of vulnerability in your own code

Corgea's AI-native static analysis detects vulnerabilities like this one across your repositories, ranks them by exploitability, and returns review-ready fixes.

Description

Summary

When pnpm processes a package's directories.bin field, it uses path.join() without validating the result stays within the package root. A malicious npm package can specify "directories": {"bin": "../../../../tmp"} to escape the package directory, causing pnpm to chmod 755 files at arbitrary locations.

Note: Only affects Unix/Linux/macOS. Windows is not affected (fixBin gated by EXECUTABLE_SHEBANG_SUPPORTED).

Details

Vulnerable code in pkg-manager/package-bins/src/index.ts:15-21:

if (manifest.directories?.bin) {
  const binDir = path.join(pkgPath, manifest.directories.bin)  // NO VALIDATION
  const files = await findFiles(binDir)
  // ... files outside package returned, then chmod 755'd
}

The bin field IS protected with isSubdir() at line 53, but directories.bin lacks this check.

PoC

# Create malicious package
mkdir /tmp/malicious-pkg
echo '{"name":"malicious","version":"1.0.0","directories":{"bin":"../../../../tmp/target"}}' > /tmp/malicious-pkg/package.json

# Create sensitive file
mkdir -p /tmp/target
echo "secret" > /tmp/target/secret.sh
chmod 600 /tmp/target/secret.sh  # Private

# Install
pnpm add file:/tmp/malicious-pkg

# Check permissions
ls -la /tmp/target/secret.sh  # Now 755 (world-readable)

Impact

  • Supply-chain attack via npm packages
  • File permissions changed from 600 to 755 (world-readable)
  • Affects non-dotfiles in predictable paths (dotfiles excluded by tinyglobby default)

Suggested Fix

Add isSubdir validation for directories.bin paths in pkg-manager/package-bins/src/index.ts, matching the existing validation in commandsFromBin():

if (manifest.directories?.bin) {
  const binDir = path.join(pkgPath, manifest.directories.bin)
  if (!isSubdir(pkgPath, binDir)) {
    return []  // Reject paths outside package
  }
  // ...
}

Ready to move

Start Securing

Free, no credit card | First findings in minutes