From 8131d7c53e5fb2bc2f9b1649d6257be729cd57c8 Mon Sep 17 00:00:00 2001 From: Dami Oyeniyi Date: Tue, 5 May 2026 13:15:21 +0100 Subject: [PATCH] fix: validate readPackage dependency maps (#11230) --- .changeset/kind-peaches-film.md | 6 +++++ hooks/pnpmfile/src/requirePnpmfile.ts | 4 ++-- .../readPackageArrayPeerDependencies.js | 8 +++++++ .../readPackageFalsyOptionalDependencies.js | 8 +++++++ .../readPackageNoObjectDevDependencies.js | 8 +++++++ hooks/pnpmfile/test/index.ts | 24 +++++++++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 .changeset/kind-peaches-film.md create mode 100644 hooks/pnpmfile/test/__fixtures__/readPackageArrayPeerDependencies.js create mode 100644 hooks/pnpmfile/test/__fixtures__/readPackageFalsyOptionalDependencies.js create mode 100644 hooks/pnpmfile/test/__fixtures__/readPackageNoObjectDevDependencies.js diff --git a/.changeset/kind-peaches-film.md b/.changeset/kind-peaches-film.md new file mode 100644 index 0000000000..ca594f9e51 --- /dev/null +++ b/.changeset/kind-peaches-film.md @@ -0,0 +1,6 @@ +--- +"@pnpm/hooks.pnpmfile": patch +"pnpm": patch +--- + +Validate all `readPackage` dependency map fields, including `devDependencies`, and reject falsy non-object invalid values instead of silently accepting them. diff --git a/hooks/pnpmfile/src/requirePnpmfile.ts b/hooks/pnpmfile/src/requirePnpmfile.ts index 7b13d96ff9..064f20f852 100644 --- a/hooks/pnpmfile/src/requirePnpmfile.ts +++ b/hooks/pnpmfile/src/requirePnpmfile.ts @@ -75,9 +75,9 @@ export async function requirePnpmfile (pnpmFilePath: string, prefix: string): Pr if (!newPkg) { throw new BadReadPackageHookError(pnpmFilePath, 'readPackage hook did not return a package manifest object.') } - const dependencies = ['dependencies', 'optionalDependencies', 'peerDependencies'] + const dependencies = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const for (const dep of dependencies) { - if (newPkg[dep] && typeof newPkg[dep] !== 'object') { + if (newPkg[dep] != null && (typeof newPkg[dep] !== 'object' || Array.isArray(newPkg[dep]))) { throw new BadReadPackageHookError(pnpmFilePath, `readPackage hook returned package manifest object's property '${dep}' must be an object.`) } } diff --git a/hooks/pnpmfile/test/__fixtures__/readPackageArrayPeerDependencies.js b/hooks/pnpmfile/test/__fixtures__/readPackageArrayPeerDependencies.js new file mode 100644 index 0000000000..1ce18cfcf6 --- /dev/null +++ b/hooks/pnpmfile/test/__fixtures__/readPackageArrayPeerDependencies.js @@ -0,0 +1,8 @@ +module.exports = { + hooks: { readPackage }, +} + +function readPackage (pkg) { + pkg.peerDependencies = [] + return pkg +} diff --git a/hooks/pnpmfile/test/__fixtures__/readPackageFalsyOptionalDependencies.js b/hooks/pnpmfile/test/__fixtures__/readPackageFalsyOptionalDependencies.js new file mode 100644 index 0000000000..2007213ffd --- /dev/null +++ b/hooks/pnpmfile/test/__fixtures__/readPackageFalsyOptionalDependencies.js @@ -0,0 +1,8 @@ +module.exports = { + hooks: { readPackage }, +} + +function readPackage (pkg) { + pkg.optionalDependencies = false + return pkg +} diff --git a/hooks/pnpmfile/test/__fixtures__/readPackageNoObjectDevDependencies.js b/hooks/pnpmfile/test/__fixtures__/readPackageNoObjectDevDependencies.js new file mode 100644 index 0000000000..cc103390d1 --- /dev/null +++ b/hooks/pnpmfile/test/__fixtures__/readPackageNoObjectDevDependencies.js @@ -0,0 +1,8 @@ +module.exports = { + hooks: { readPackage }, +} + +function readPackage (pkg) { + pkg.devDependencies = '@oclif/errors' + return pkg +} diff --git a/hooks/pnpmfile/test/index.ts b/hooks/pnpmfile/test/index.ts index 6188c719db..f34a74b06b 100644 --- a/hooks/pnpmfile/test/index.ts +++ b/hooks/pnpmfile/test/index.ts @@ -32,6 +32,30 @@ test('readPackage hook run fails when returned dependencies is not an object', a ).rejects.toEqual(new BadReadPackageHookError(pnpmfilePath, 'readPackage hook returned package manifest object\'s property \'dependencies\' must be an object.')) }) +test('readPackage hook run fails when returned devDependencies is not an object', async () => { + const pnpmfilePath = path.join(import.meta.dirname, '__fixtures__/readPackageNoObjectDevDependencies.js') + const { pnpmfileModule: pnpmfile } = (await requirePnpmfile(pnpmfilePath, import.meta.dirname))! + return expect( + pnpmfile!.hooks!.readPackage!({}, defaultHookContext) + ).rejects.toEqual(new BadReadPackageHookError(pnpmfilePath, 'readPackage hook returned package manifest object\'s property \'devDependencies\' must be an object.')) +}) + +test('readPackage hook run fails when returned optionalDependencies is a falsy non-object value', async () => { + const pnpmfilePath = path.join(import.meta.dirname, '__fixtures__/readPackageFalsyOptionalDependencies.js') + const { pnpmfileModule: pnpmfile } = (await requirePnpmfile(pnpmfilePath, import.meta.dirname))! + return expect( + pnpmfile!.hooks!.readPackage!({}, defaultHookContext) + ).rejects.toEqual(new BadReadPackageHookError(pnpmfilePath, 'readPackage hook returned package manifest object\'s property \'optionalDependencies\' must be an object.')) +}) + +test('readPackage hook run fails when returned peerDependencies is an array', async () => { + const pnpmfilePath = path.join(import.meta.dirname, '__fixtures__/readPackageArrayPeerDependencies.js') + const { pnpmfileModule: pnpmfile } = (await requirePnpmfile(pnpmfilePath, import.meta.dirname))! + return expect( + pnpmfile!.hooks!.readPackage!({}, defaultHookContext) + ).rejects.toEqual(new BadReadPackageHookError(pnpmfilePath, 'readPackage hook returned package manifest object\'s property \'peerDependencies\' must be an object.')) +}) + test('filterLog hook combines with the global hook', async () => { const globalPnpmfile = path.join(import.meta.dirname, '__fixtures__/globalFilterLog.js') const pnpmfile = path.join(import.meta.dirname, '__fixtures__/filterLog.js')