{"id":"CVE-2026-59194","aliases":["EUVD-2026-41881","GHSA-72r4-9c5j-mj57","ROOT-APP-NPM-GHSA-72r4-9c5j-mj57"],"status":"published","title":"pnpm: patch-remove could delete project-selected files outside the patches directory","description":"pnpm is a package manager. Prior to 10.34.4 and 11.7.0, a crafted patch entry could resolve outside the configured patches directory and cause pnpm patch-remove to delete an arbitrary reachable file. This vulnerability is fixed in 10.34.4 and 11.7.0.","severity":{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00401,"percentile":0.34848,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-22","CWE-73"],"published":"2026-06-27T00:12:39Z","modified":"2026-10-28T02:31:11Z","fixes":[{"ecosystem":"npm","package":"pnpm","fixed_in":["10.34.4","11.7.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.7.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.7.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-72r4-9c5j-mj57","tags":["x_refsource_CONFIRM","Vendor Advisory","Exploit","advisory","web"]},{"url":"https://github.com/pnpm/pnpm/commit/612a2e6a7333f2b061f452a21b6e62c1c161747f","tags":["web"]},{"url":"http://github.com/pnpm/pnpm/commit/352ae489f1b14ffdc19d2c6eacb1b06b098c2ddc","tags":["web"]},{"url":"https://github.com/advisories/GHSA-72r4-9c5j-mj57"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/59xxx/CVE-2026-59194.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59194","tags":["advisory"]},{"url":"https://github.com/pnpm/pnpm","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-59194","title":"pnpm: patch-remove could delete project-selected files outside the patches directory","description":"pnpm is a package manager. Prior to 10.34.4 and 11.7.0, a crafted patch entry could resolve outside the configured patches directory and cause pnpm patch-remove to delete an arbitrary reachable file. This vulnerability is fixed in 10.34.4 and 11.7.0.","status":"published","published":"2026-07-06T15:16:28.485Z","modified":"2026-07-07T16:57:59.326Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high","assessor":"cna"}],"cwes":["CWE-22","CWE-73"],"updated_at":"2026-11-02T14:19:58.560602Z"}],"euvd":[{"id":"EUVD-2026-41881","description":"pnpm is a package manager. Prior to 10.34.4 and 11.7.0, a crafted patch entry could resolve outside the configured patches directory and cause pnpm patch-remove to delete an arbitrary reachable file. This vulnerability is fixed in 10.34.4 and 11.7.0.","status":"published","published":"2026-07-06T15:16:28Z","modified":"2026-07-07T16:57:59Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"updated_at":"2026-11-02T18:20:11.442824Z"}],"ghsa":[{"id":"GHSA-72r4-9c5j-mj57","title":"pnpm: `patch-remove` could delete project-selected files outside the patches directory","description":"## Summary\n\nThe `patch-remove` deletion-scope issue tracked as GHSA-72r4-9c5j-mj57 / CAND-PNPM-030 has been addressed in pnpm.\n\nA crafted patch entry could resolve outside the configured patches directory and cause `pnpm patch-remove` to delete an arbitrary reachable file. This patch validates the configured directory and every resolved target before unlinking anything, then deletes the final directory entry without following it.\n\n## Security boundary\n\n- Traversal and absolute paths that resolve outside the configured patches directory are rejected before deletion.\n- Parent directories are canonicalized before deletion, including the case where a nested symlink points outside and the final outside entry is itself dangling.\n- The complete batch is validated before any file is removed.\n- Component-aware predicates accept valid names beginning with `..` while still rejecting parent traversal, Windows drive escapes, and UNC escapes.\n- Valid files and symlinked patch directories whose canonical targets remain below the lockfile directory continue to work.\n- A final symlink inside a valid patch directory is unlinked without following its target, including when the target is outside or dangling.\n\n## Exploit replay\n\nBefore the patch, a workspace `patchedDependencies` path that resolved outside the project caused `pnpm patch-remove` to delete the external sentinel. A second replay used a nested parent symlink and a dangling outside victim: `realpath()` returned `ENOENT`, yet the victim was still removed. With this patch, both paths are rejected and the outside entries remain intact.\n\n## Files changed\n\n- `patching/commands/src/isSubdirectory.ts` performs component-aware containment checks.\n- `patching/commands/src/patchRemove.ts` validates the full batch, canonicalizes parents, and unlinks final entries without following them.\n- `patching/commands/test/{isSubdirectory,patchRemove}.test.ts` covers traversal, nested symlinks, dangling victims, and valid removals.\n\n## Commands run\n\n```text\n$ pnpm --filter @pnpm/patching.commands test test/isSubdirectory.test.ts test/patchRemove.test.ts\nPASS: 11 tests across 2 suites\n$ pnpm --filter @pnpm/patching.commands run compile\nPASS\n$ git diff --check\nPASS\n```\n\n## Validation\n\n- Focused handler and path-predicate suites: 11 passed across 2 suites.\n- Package-wide ESLint: passed.\n- Package TypeScript build: passed.\n- Commit hooks, Commitlint, and `git diff --check`: passed.\n- The broader integration harness was environment-blocked because it writes outside the available temporary root; focused handler tests used `/private/tmp`.\n\n## Patches\n\n`10.34.4`: https://github.com/pnpm/pnpm/commit/352ae489f1b14ffdc19d2c6eacb1b06b098c2ddc\n`11.7.0`: https://github.com/pnpm/pnpm/commit/612a2e6a7333f2b061f452a21b6e62c1c161747f\n\n## Compatibility\n\nMissing patch files remain no-ops. Valid symlinked patch directories continue to work when their canonical target stays inside the lockfile directory, and final symlinks are removed without touching their targets. `patch-remove` is not yet in pacquet's command surface, so no Rust-side parity change is required.\n\n## Remaining risk\n\nPortable Node APIs do not expose directory-fd-relative `unlinkat()`. A local attacker who can replace an already validated parent directory before the unlink may still win a time-of-check/time-of-use race. The reproduced repository-controlled traversal and symlink paths do not require that concurrent capability and are blocked by this patch.\n\n---\nWritten by an agent (Codex, GPT-5).","status":"published","published":"2026-06-27T00:12:39Z","modified":"2026-06-27T00:12:40Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22","CWE-73"],"updated_at":"2026-11-02T14:40:34.713036Z"}],"nvd":[{"id":"CVE-2026-59194","description":"pnpm is a package manager. Prior to 10.34.4 and 11.7.0, a crafted patch entry could resolve outside the configured patches directory and cause pnpm patch-remove to delete an arbitrary reachable file. This vulnerability is fixed in 10.34.4 and 11.7.0.","status":"published","published":"2026-07-06T16:16:37.55Z","modified":"2026-07-07T19:08:57.837Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high","assessor":"secondary"}],"cwes":["CWE-22","CWE-73"],"updated_at":"2026-11-02T14:12:28.040501Z"}],"osv":[{"id":"CVE-2026-59194","title":"pnpm: patch-remove could delete project-selected files outside the patches directory","description":"pnpm is a package manager. Prior to 10.34.4 and 11.7.0, a crafted patch entry could resolve outside the configured patches directory and cause pnpm patch-remove to delete an arbitrary reachable file. This vulnerability is fixed in 10.34.4 and 11.7.0.","status":"published","published":"2026-07-06T15:16:28.485Z","modified":"2026-10-28T02:31:11.78042212Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22","CWE-73"],"updated_at":"2026-11-02T15:45:12.496548Z"},{"id":"GHSA-72r4-9c5j-mj57","title":"pnpm: `patch-remove` could delete project-selected files outside the patches directory","description":"## Summary\n\nThe `patch-remove` deletion-scope issue tracked as GHSA-72r4-9c5j-mj57 / CAND-PNPM-030 has been addressed in pnpm.\n\nA crafted patch entry could resolve outside the configured patches directory and cause `pnpm patch-remove` to delete an arbitrary reachable file. This patch validates the configured directory and every resolved target before unlinking anything, then deletes the final directory entry without following it.\n\n## Security boundary\n\n- Traversal and absolute paths that resolve outside the configured patches directory are rejected before deletion.\n- Parent directories are canonicalized before deletion, including the case where a nested symlink points outside and the final outside entry is itself dangling.\n- The complete batch is validated before any file is removed.\n- Component-aware predicates accept valid names beginning with `..` while still rejecting parent traversal, Windows drive escapes, and UNC escapes.\n- Valid files and symlinked patch directories whose canonical targets remain below the lockfile directory continue to work.\n- A final symlink inside a valid patch directory is unlinked without following its target, including when the target is outside or dangling.\n\n## Exploit replay\n\nBefore the patch, a workspace `patchedDependencies` path that resolved outside the project caused `pnpm patch-remove` to delete the external sentinel. A second replay used a nested parent symlink and a dangling outside victim: `realpath()` returned `ENOENT`, yet the victim was still removed. With this patch, both paths are rejected and the outside entries remain intact.\n\n## Files changed\n\n- `patching/commands/src/isSubdirectory.ts` performs component-aware containment checks.\n- `patching/commands/src/patchRemove.ts` validates the full batch, canonicalizes parents, and unlinks final entries without following them.\n- `patching/commands/test/{isSubdirectory,patchRemove}.test.ts` covers traversal, nested symlinks, dangling victims, and valid removals.\n\n## Commands run\n\n```text\n$ pnpm --filter @pnpm/patching.commands test test/isSubdirectory.test.ts test/patchRemove.test.ts\nPASS: 11 tests across 2 suites\n$ pnpm --filter @pnpm/patching.commands run compile\nPASS\n$ git diff --check\nPASS\n```\n\n## Validation\n\n- Focused handler and path-predicate suites: 11 passed across 2 suites.\n- Package-wide ESLint: passed.\n- Package TypeScript build: passed.\n- Commit hooks, Commitlint, and `git diff --check`: passed.\n- The broader integration harness was environment-blocked because it writes outside the available temporary root; focused handler tests used `/private/tmp`.\n\n## Patches\n\n`10.34.4`: https://github.com/pnpm/pnpm/commit/352ae489f1b14ffdc19d2c6eacb1b06b098c2ddc\n`11.7.0`: https://github.com/pnpm/pnpm/commit/612a2e6a7333f2b061f452a21b6e62c1c161747f\n\n## Compatibility\n\nMissing patch files remain no-ops. Valid symlinked patch directories continue to work when their canonical target stays inside the lockfile directory, and final symlinks are removed without touching their targets. `patch-remove` is not yet in pacquet's command surface, so no Rust-side parity change is required.\n\n## Remaining risk\n\nPortable Node APIs do not expose directory-fd-relative `unlinkat()`. A local attacker who can replace an already validated parent directory before the unlink may still win a time-of-check/time-of-use race. The reproduced repository-controlled traversal and symlink paths do not require that concurrent capability and are blocked by this patch.\n\n---\nWritten by an agent (Codex, GPT-5).","status":"published","published":"2026-06-27T00:12:39Z","modified":"2026-07-08T08:27:15.664767504Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-22","CWE-73"],"updated_at":"2026-11-02T15:46:40.308826Z"},{"id":"ROOT-APP-NPM-GHSA-72r4-9c5j-mj57","title":"GHSA-72r4-9c5j-mj57 in @rootio/pnpm - Patched by Root","description":"Root has patched GHSA-72r4-9c5j-mj57 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.375720736Z","updated_at":"2026-11-02T15:53:27.940619Z"}]}}
