Jun 27, 2026·6 min read·4 visits
A validation flaw in pnpm allows malicious packages to register '..' as a binary command name. When the package is uninstalled or updated, pnpm evaluates this command key, resolves it to the parent directory of global binaries (typically PNPM_HOME), and deletes it recursively.
CVE-2026-55699 (also identified as GHSA-4gxm-v5v7-fqc4) is a critical path traversal and arbitrary directory deletion vulnerability in the pnpm package manager. The issue exists because the manifest validation process fails to prevent relative path segments within the package 'bin' keys. When a malicious package containing structured path traversal markers is globally installed and later manipulated, pnpm resolves the target paths through path.join() and passes the resolved paths to a recursive deletion function, resulting in arbitrary directory removal.
The Node.js package manager pnpm supports global binary script generation using the bin field of a package manifest. When globally installing a package, pnpm reads this field, matches the designated key with an execution script, and populates symlinks or binary execution shims within the global binary directory (globalBinDir). This exposes an attack surface where packages exercise control over local filesystem link paths.
The vulnerability is classified under CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) and CWE-73 (External Control of File Name or Path). It affects the validation engine of both the Node.js TypeScript codebase and the Rust-based pacquet command-shim parser. Due to insufficient sanitization, reserved relative path parameters can be stored as valid execution binaries.
Because pnpm uses a local index of installed binaries for maintenance tasks, any malicious command entry is evaluated during subsequent package workflows. If a user uninstalls, overrides, or updates the malicious package, pnpm executes recursive directory removal on the resolved path. This allows unauthenticated, remote attackers to trigger the deletion of arbitrary host directories where the running process has adequate permissions.
The root cause of CVE-2026-55699 is a failure to properly sanitize keys in the bin map of a package's package.json file. The flaw resides in two modules: the TypeScript-based bins/resolver/src/index.ts and the Rust-based pacquet/crates/cmd-shim/src/bin_resolver.rs parity engine.
First, the validation routine verifies command keys using a basic regex validation that checks for URL safety or prefix markers. However, it does not explicitly ban reserved relative directory paths such as "", ".", or "..". This permits structural markers to satisfy the primary regex parser.
Second, the scope-stripping function introduces an evasion vector. When resolving scoped packages (such as @scope/package), pnpm strips the organizational scope. If a manifest defines a binary key like "@scope/..", the stripping function processes the value, removes @scope/, and yields "..". This derived string bypasses validation boundaries because the stripping occurs after or in a manner that isolates it from validation checks.
Finally, when global package management operations (e.g., pnpm global remove) run, pnpm looks up the binary registry. It attempts to map the deletion target using path.join(globalBinDir, binName). If binName is "..", the path resolves to the parent of the binary directory. This resolved string is passed to removeBin() within bins/remover/src/removeBins.ts, which executes recursive directory deletion via fs.rmSync(targetBinPath, { recursive: true, force: true }) without enforcing path confinement.
The vulnerable code path is illustrated by analyzing the resolution logic. Prior to the patch, the resolver evaluated the keys without confirming that the output path remained within the bounds of the global installation directory.
// Vulnerable logic flow (Conceptual TypeScript)
import * as path from 'path';
export function resolveBinTarget(globalBinDir: string, binKey: string): string {
// Bypassed if key is '@scope/..' because stripScope removes '@scope/'
const cleanBinKey = stripScope(binKey);
// Lack of path containment validation
return path.join(globalBinDir, cleanBinKey);
}The corresponding Rust resolution logic in pacquet exhibited a parallel vulnerability where the cmd-shim implementation resolved paths without checking if the binary name resolved to a directory boundary outside of the configured destination root.
// Patched TypeScript resolution logic
import * as path from 'path';
export function resolveBinTarget(globalBinDir: string, binKey: string): string {
const cleanBinKey = stripScope(binKey);
// Strict validation prevents directory traversal names
if (cleanBinKey === '' || cleanBinKey === '.' || cleanBinKey === '..') {
throw new Error(`Invalid binary execution key: ${binKey}`);
}
const resolvedPath = path.resolve(globalBinDir, cleanBinKey);
// Defense-in-depth confinement verification
if (!resolvedPath.startsWith(globalBinDir)) {
throw new Error('Access Denied: Path traversal detected outside of the target binary root');
}
return resolvedPath;
}The fix introduces strict checks inside both the TypeScript module and the Rust Pacquet bin_resolver.rs engine. Keys resolving to relative directory identifiers are rejected, and directory confinement checks block any execution targets that attempt to step outside the designated boundary.
Exploitation relies on an attacker publishing a package with a malformed package.json to a registry reachable by the target user. The attack sequence consists of three phases: publishing, user-initiated global installation, and execution of a management workflow that triggers cleanup.
The attacker crafts a manifest where the bin configuration registers a double-dot or scoped relative key. The following example targets the parent directory of globalBinDir during uninstallation:
{
"name": "@attacker/poc-scope-bypass",
"version": "1.0.0",
"bin": {
"@attacker/..": "./exploit.js"
}
}When the victim installs this package globally using pnpm add -g @attacker/poc-scope-bypass, pnpm registers the package and caches its configuration. At this stage, no deletion occurs. The deletion triggers when the victim runs pnpm remove -g @attacker/poc-scope-bypass or executes an update that replaces the package. pnpm reads the cached manifest, joins globalBinDir with the evaluated ".." string, and calls the uninstaller. The uninstaller invokes recursive deletion on the resolved parent path, erasing the user configuration directory.
The impact of CVE-2026-55699 is classified as High for availability. Because the uninstallation processes rely on recursive file system removal (fs.rmSync with recursive and force parameters active), the execution results in immediate, complete erasure of the resolved target directory.
If the system is running standard configurations, globalBinDir is nested within PNPM_HOME. A value of ".." maps directly to PNPM_HOME, causing the silent deletion of all active global shims, downloaded store metadata, shell configurations, and adjacent global execution tools. This renders all installed global packages unusable and disrupts development configurations.
Because the vulnerability is triggered by a package manager running in the user space, the scope is restricted to directories accessible to the executing user's privileges. No data exposure or privilege escalation has been identified. However, if pnpm is executed in CI/CD pipeline runners or automated build nodes with administrative or system privileges, the path traversal could delete critical system directories.
To resolve this vulnerability, users must update to fixed versions of pnpm. If immediate update cycles are restricted by environment controls, development teams should apply temporary mitigation strategies.
Ensure that all installations are updated according to the product release branch. For the 10.x release line, users must migrate to 10.34.2 or higher. For the 11.x release line, users must migrate to 11.5.3 or higher. These updates contain the patched validation mechanisms in both TypeScript and the Rust package resolver.
Where upgrading is postponed, organizations should enforce package installation controls. Restrict the global installation of packages from untrusted or public registries, and audit the configuration of custom package mirrors. Running static analysis checks on incoming packages can block manifests containing relative path descriptors in binary registration keys.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
pnpm pnpm | < 10.34.2 | 10.34.2 |
pnpm pnpm | >= 11.0.0 < 11.5.3 | 11.5.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22, CWE-73 |
| Attack Vector | Network (Requires User Interaction) |
| CVSS v3.1 Score | 6.5 |
| EPSS Score | 0.00271 |
| Impact | High (Availability - Arbitrary Directory Deletion) |
| Exploit Status | Proof of Concept (PoC) available |
| KEV Status | Not listed |
The software uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize special elements such as '..' that can resolve to locations outside of the restricted directory.
A path traversal vulnerability in pnpm stage download allows malicious registries or compromised package manifests to overwrite arbitrary files on the victim's filesystem via unvalidated package name and version fields.
GHSA-WW5P-J6CJ-6MQQ is a technical credential exposure vulnerability in Nezha Dashboard prior to version 2.2.5. The vulnerability allows authenticated administrative users or actors possessing scoped read-only Personal Access Tokens (PATs) to exfiltrate plaintext third-party API credentials, secret keys, and webhook authorization headers due to a lack of data redaction during API object serialization.
GHSA-FR4H-3CPH-29XV is a high-severity path traversal vulnerability in pnpm and its Rust-based port pacquet. The flaw manifests when using the hoisted node-linker configuration, allowing an attacker to manipulate the lockfile to resolve relative traversal sequences or target reserved subdirectories, leading to arbitrary file write or execution hijacking.
A path traversal vulnerability in the pnpm package manager's 'patch-remove' command allows an attacker to delete arbitrary files outside the patches directory. By manipulating configuration files like package.json, an attacker can specify a traversal path that the application deletes recursively without validating the path's containment.
A high-severity path traversal vulnerability exists in the pnpm package manager. By crafting a malicious lockfile (pnpm-lock.yaml) with path traversal characters in the configDependencies block, an attacker can create arbitrary directories and symlinks outside the project's node_modules/.pnpm-config directory. This exploitation happens automatically during pnpm installation, even when executing with scripts disabled via the --ignore-scripts flag.
An arbitrary file write vulnerability exists in Gonic, a music streaming server implementing the Subsonic API. Due to an unreachable guard clause combined with missing path containment validation in the playlist storage engine, authenticated users can write playlist contents to arbitrary filesystem paths with overly permissive directory permissions.