mirror of
https://github.com/pnpm/pnpm.git
synced 2026-02-15 01:24:07 -05:00
fix: prevent path traversal by validating bin names
This commit is contained in:
6
.changeset/kind-boxes-shake.md
Normal file
6
.changeset/kind-boxes-shake.md
Normal 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.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user