CVE Patch Review

GHSA-QRV3-253H-G69C: Root-cause Fix for pnpm configDependencies Path Traversal

GHSA-QRV3-253H-G69C · Updated 2026-06-27 Root-cause

Summary

The patch addresses the core filesystem trust issue by validating config dependency names and versions before they are converted into path segments, and by hardening hoisted alias placement with sink-side npm-name validation. This closes the documented path traversal and arbitrary symlink creation vector from crafted configDependencies and reduces related lockfile-driven alias abuse at node_modules sinks.

Analysis

Vulnerability

GHSA-QRV3-253H-G69C describes a path traversal issue in pnpm's configDependencies handling. Per the patch notes, dependency names and versions were used to construct filesystem paths under node_modules/.pnpm-config and the store without sufficient validation, allowing traversal-shaped values such as ../../PWNED or ../../../PWNED to escape intended install boundaries and create files or symlinks outside the managed area. The commit summary explicitly states that this could bypass the execution boundary expectations of --ignore-scripts by still causing filesystem side effects during install via crafted workspace metadata or lockfile-derived aliases.

The root cause is unsafe use of untrusted package metadata as path segments. The vulnerable behavior was not limited to a single join call: config dependency names became directory names, versions became store path segments, and hoisted aliases reconstructed from lockfiles could be joined under node_modules. A pure containment check is insufficient for reserved internal names such as .bin, .pnpm, and node_modules, because those may still resolve inside the install root while overwriting pnpm-owned layout. The advisory and patch together show that both traversal and namespace collision had to be addressed at the validation boundary and at the filesystem sink.

const configDeps: Record<string, string> = {
  '../../PWNED': `100.0.0+${getIntegrity('@pnpm.e2e/foo', '100.0.0')}`,
}

const configDeps2: Record<string, string> = {
  '@pnpm.e2e/foo': `../../../PWNED+${getIntegrity('@pnpm.e2e/foo', '100.0.0')}`,
}

Sources: official patch commit, GitHub Security Advisory, third-party report.

Patch

The patch introduces explicit validation in the config dependency installer and strengthens alias validation in the symlink dependency layer. In config/deps-installer, pnpm adds validate-npm-package-name and semver dependencies, then validates each config dependency name with assertValidConfigDepName() and each version with assertValidConfigDepVersion() before any path construction occurs. The name validator rejects traversal-shaped names, reserved names, and __proto__; the version validator requires an exact semver version, preventing traversal strings from becoming store path segments.

In fs/symlink-dependency, the patch adds safeJoinModulesDir as an exported primitive and upgrades it from a containment-only guard to a combined npm-name validation plus containment check. This is important because lockfile- or snapshot-derived aliases may bypass earlier manifest-time validation. The new logic rejects aliases that are not valid npm package names even if they would resolve inside node_modules, covering reserved internal directories that a path-prefix check alone would miss.

export function assertValidConfigDepName (name: string): void {
  if (!validateNpmPackageName(name).validForOldPackages) {
    throw new PnpmError(
      'INVALID_DEPENDENCY_NAME',
      `The configDependencies in pnpm-workspace.yaml contains a dependency with an invalid name: ${JSON.stringify(name)}`
    )
  }
}

export function assertValidConfigDepVersion (name: string, version: string): void {
  if (semver.valid(version) == null) {
    throw new PnpmError(
      'INVALID_CONFIG_DEP_VERSION',
      `The config dependency "${name}" has an invalid version "${version}"`
    )
  }
}

The tests are aligned with the threat model. New cases verify rejection of traversal names, traversal versions, and __proto__, and assert that no PWNED entry appears in either the working directory or store. Additional symlink tests reject .bin, .pnpm, and node_modules aliases while preserving valid aliases such as foo and @scope/name. Source: official patch commit.

Review

Pros

  • The fix addresses the actual trust-boundary failure: untrusted metadata is validated before being used as a filesystem path component.
  • Name validation uses established npm package-name rules via validate-npm-package-name, which blocks traversal strings, leading-dot names, reserved names, and __proto__.
  • Version validation constrains config dependency versions to exact semver, eliminating traversal-shaped store path segments.
  • The sink-side hardening in safeJoinModulesDir is well-placed. It protects lockfile- and snapshot-derived aliases even if earlier validation layers are bypassed.
  • The retained containment check provides defense in depth for platform-specific path resolution behavior.
  • Regression tests are concrete and security-relevant, checking both rejection behavior and absence of escaped filesystem artifacts.

Cons

  • The patch scope appears broader than the advisory title, including hoisted alias containment and an unrelated patch-remove changeset note. That is good for security posture but can complicate backport review.
  • Requiring exact semver for config dependency versions is intentionally restrictive; if prior behavior tolerated ranges or non-semver encodings, this is a compatibility change that downstream users may notice.
  • The reviewable snippets do not show every call site of safeJoinModulesDir, so full assurance depends on the commit-wide integration rather than the isolated helper alone.

Verdict

Root-cause.

This patch fixes the core issue rather than masking symptoms. The vulnerable condition was path construction from untrusted config dependency names, versions, and lockfile-derived aliases. The patch directly constrains those values to valid package-name and exact-semver domains before path creation, and it adds sink-side validation where reconstructed aliases are materialized under node_modules. That combination closes both traversal escape and reserved-layout overwrite paths documented in the commit notes and advisory. Based on the provided sources, this is a technically sound and source-grounded remediation for GHSA-QRV3-253H-G69C.

Sources