pnpm scoped bin name Path Traversal allows arbitrary file creation outside node_modules/.bin
Description
pnpm is a package manager. Prior to version 10.28.1, a path traversal vulnerability in pnpm's bin linking allows malicious npm packages to create executable shims or symlinks outside of node_modules/.bin. Bin names starting with @ bypass validation, and after scope normalization, path traversal sequences like ../../ remain intact. This issue affects all pnpm users who install npm packages and CI/CD pipelines using pnpm. It can lead to overwriting config files, scripts, or other sensitive files. Version 10.28.1 contains a patch.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pnpmnpm | < 10.28.1 | 10.28.1 |
Affected products
1Patches
18afbb1598445fix: prevent path traversal by validating bin names
3 files changed · +40 −19
.changeset/kind-boxes-shake.md+6 −0 added@@ -0,0 +1,6 @@ +--- +"@pnpm/package-bins": patch +"pnpm": patch +--- + +Fixed a path traversal vulnerability in pnpm's bin linking. Bin names starting with `@` bypassed validation, and after scope normalization, path traversal sequences like `../../` remained intact. \ No newline at end of file
pkg-manager/package-bins/src/index.ts+16 −19 modified@@ -39,24 +39,21 @@ async function findFiles (dir: string): Promise<string[]> { } } -function commandsFromBin (bin: PackageBin, pkgName: string, pkgPath: string) { - if (typeof bin === 'string') { - return [ - { - name: normalizeBinName(pkgName), - path: path.join(pkgPath, bin), - }, - ] +function commandsFromBin (bin: PackageBin, pkgName: string, pkgPath: string): Command[] { + const cmds: Command[] = [] + for (const [commandName, binRelativePath] of typeof bin === 'string' ? [[pkgName, bin]] : Object.entries(bin)) { + const binName = commandName[0] === '@' + ? commandName.slice(commandName.indexOf('/') + 1) + : commandName + // Validate: must be safe (no path traversal) - only allow URL-safe chars or $ + if (binName !== encodeURIComponent(binName) && binName !== '$') { + continue + } + const binPath = path.join(pkgPath, binRelativePath) + if (!isSubdir(pkgPath, binPath)) { + continue + } + cmds.push({ name: binName, path: binPath }) } - return Object.keys(bin) - .filter((commandName) => encodeURIComponent(commandName) === commandName || commandName === '$' || commandName[0] === '@') - .map((commandName) => ({ - name: normalizeBinName(commandName), - path: path.join(pkgPath, bin[commandName]), - })) - .filter((cmd) => isSubdir(pkgPath, cmd.path)) -} - -function normalizeBinName (name: string): string { - return name[0] === '@' ? name.slice(name.indexOf('/') + 1) : name + return cmds }
pkg-manager/package-bins/test/index.ts+18 −0 modified@@ -126,3 +126,21 @@ test('get bin from scoped bin name', async () => { ] ) }) + +test('skip scoped bin names with path traversal', async () => { + expect( + await getBinsFromPackageManifest({ + name: 'malicious', + version: '1.0.0', + bin: { + '@scope/../../.npmrc': './malicious.js', + '@scope/../etc/passwd': './evil.js', + '@scope/legit': './good.js', + }, + }, process.cwd())).toStrictEqual([ + { + name: 'legit', + path: path.resolve('good.js'), + }, + ]) +})
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-xpqm-wm3m-f34hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-23890ghsaADVISORY
- github.com/pnpm/pnpm/commit/8afbb1598445d37985d91fda18abb4795ae5062dghsax_refsource_MISCWEB
- github.com/pnpm/pnpm/releases/tag/v10.28.1ghsax_refsource_MISCWEB
- github.com/pnpm/pnpm/security/advisories/GHSA-xpqm-wm3m-f34hghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.