fix: prevent path traversal by validating bin names

This commit is contained in:
Zoltan Kochan
2026-01-15 17:07:09 +01:00
parent 6e4b09415f
commit 8afbb15984
3 changed files with 40 additions and 19 deletions

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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'),
},
])
})