CVE-2026-55700: Root-Cause Fix for pnpm stage download Path Traversal
Summary
The patch addresses arbitrary file write during `pnpm stage download` by validating manifest-derived `name` and `version` before filename construction, centralizing tarball filename generation, and adding traversal-focused tests. The fix is strong because it removes trust in unvalidated manifest fields and adds both semantic validation and path-boundary checks before writing to disk.
Analysis
Vulnerability
CVE-2026-55700 describes a path traversal and arbitrary file write condition in pnpm stage download. The vulnerable flow derived an output filename directly from untrusted package manifest fields embedded in the downloaded tarball. Specifically, summary.name and summary.version were interpolated into the destination filename with only minimal package-name normalization, and the result was passed to path.resolve(...) before writing the tarball to disk. Because version was not sanitized and name normalization only replaced @ and the first /, attacker-controlled manifest content could introduce path separators or traversal segments and escape the intended download directory.
The vulnerable code path shown in the upstream patch reference at pnpm/pnpm#12303 was:
const filename = `${normalizePackageName(summary.name)}-${summary.version}-${stageId}.tgz`
await fs.writeFile(path.resolve(opts.dir ?? process.cwd(), filename), tarballData)This is a classic trust-boundary failure: metadata extracted from a remote artifact was treated as safe for filesystem path construction. The CVE context is also reflected in the MITRE CVE record.
Patch
The patch replaces ad hoc filename construction with a centralized helper, createTarballFilename(), and uses explicit validation before any write occurs. The new helper validates package names with validate-npm-package-name, validates versions with semver.valid(), and rejects any resulting filename whose POSIX or Windows basename differs from the full string. The download path is then resolved and checked to ensure its parent directory is exactly the intended download directory.
export function createTarballFilename ({ name, version, suffix }: CreateTarballFilenameOptions): string {
if (!validateNpmPackageName(name).validForOldPackages) {
throw new PnpmError('INVALID_PACKAGE_NAME', `Invalid package name "${name}".`)
}
if (!valid(version)) {
throw new PnpmError('INVALID_PACKAGE_VERSION', `Invalid package version "${version}".`)
}
const filename = `${normalizePackageName(name)}-${version}${suffix == null ? '' : `-${suffix}`}.tgz`
if (path.basename(filename) !== filename || path.win32.basename(filename) !== filename) {
throw new PnpmError('INVALID_TARBALL_FILENAME', `Invalid tarball filename "${filename}".`)
}
return filename
}In stage/download.ts, the write path now becomes:
const filename = createTarballFilename({ name: summary.name, version: summary.version, suffix: stageId })
const downloadDir = path.resolve(opts.dir ?? process.cwd())
const outputPath = path.resolve(downloadDir, filename)
if (path.dirname(outputPath) !== downloadDir) {
throw new PnpmError('INVALID_TARBALL_FILENAME', `Invalid tarball filename "${filename}".`)
}
await fs.writeFile(outputPath, tarballData)The patch also removes duplicate filename logic by re-exporting normalizePackageName from the new helper module and updating tarball summarization to use the same safe constructor. Finally, regression tests were added for traversal attempts through both manifest version and name, asserting that no file is created outside the target directory. These changes are documented in the upstream change summary at the patch PR.
Review
Pros
- The patch fixes the root trust issue by validating manifest-derived
nameandversionbefore filename generation rather than trying to clean up dangerous strings after the fact. - Centralizing filename construction in
safeTarballFilename.tsreduces the chance of future call sites reintroducing inconsistent or weaker logic. - Using both semantic validation and basename checks is defense in depth: invalid package metadata is rejected early, and any unexpected separator behavior across POSIX or Windows is also screened.
- The additional
path.dirname(outputPath) === downloadDirassertion provides a final containment check at the write boundary. - Tests are well targeted: they simulate malicious tarballs and verify both the thrown error codes and the absence of filesystem side effects outside the download directory.
- The same safe filename generation is applied to tarball summarization, reducing divergence between display and write paths.
Cons
normalizePackageName()still performs only simple character replacement, so safety depends primarily on prior package-name validation. That is acceptable here, but the helper remains unsuitable if reused with unvalidated input elsewhere.- The directory containment check compares
path.dirname(outputPath)todownloadDir, which is effective for single-file writes but narrower than a generalized relative-path containment check. If future logic ever permits nested output paths intentionally, this guard would need redesign. - The patch validates
nameagainstvalidForOldPackages. That matches broad npm compatibility, but it is a policy choice; stricter validation could further reduce edge cases if backward compatibility were not required.
Verdict
Root-cause.
This patch addresses the vulnerability at the correct layer by treating tarball manifest metadata as untrusted input and enforcing validity before deriving a filesystem path. It also adds a write-boundary check and regression tests that directly exercise the exploit primitive described in NVD. For the documented issue in pnpm stage download, the remediation is technically sound and materially reduces the chance of recurrence through the same code path.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- Path Traversal.js
Closest JavaScript/Node-aligned hands-on defensive lab for the core weakness behind CVE-2026-55700: unsanitized path components leading to filesystem escape and arbitrary write/read risks. Good for practicing canonicalization, validation, and safe path joining before file operations.
- Path Traversal II.js
A stronger follow-on JavaScript lab that likely requires deeper defense-in-depth fixes, which maps well to patch review work like pnpm's manifest field sanitization. Useful for reinforcing secure handling of untrusted filenames and preventing directory breakout in more realistic flows.
- Slipstream.js
Best match for the arbitrary file write/archive extraction angle. Although framed as Zip Slip, it closely parallels package/download-stage write bugs where attacker-controlled archive or metadata paths can escape intended directories. Useful for learning safe extraction and write-boundary enforcement.