mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-18 22:02:53 -04:00
Resolves the 15 open alerts on https://github.com/pnpm/pnpm/security/code-scanning by addressing all four categories that CodeQL flagged. ### Prototype-polluting assignment (3 alerts, product code) - `pkg-manifest/utils/src/convertEnginesRuntimeToDependencies.ts`: the inner write now dispatches over a literal `switch` on `runtimeName`, so the assignment is always keyed by `'node' | 'deno' | 'bun'`. - `pkg-manifest/utils/src/updateProjectManifestObject.ts`: added an `isProtoPollutionKey` barrier at the top of the loop so `packageSpec.alias` can never reach the dynamic property write with `__proto__` / `constructor` / `prototype`. - `installing/deps-installer/src/uninstall/removeDeps.ts`: the package list is filtered through `isProtoPollutionKey` once up front, and the dependency record is captured into a local before the loop. ### Polynomial ReDoS (2 alerts) - `deps/inspection/list/src/renderDependentsTree.ts`: `replace(/\n+$/, '')` swapped for a constant-time `charCodeAt` trim. - `resolving/npm-resolver/src/fetch.ts`: removed the super-linear-backtracking `semverRegex` and replaced it with an O(n) `stripTrailingSemverSuffix` that splits on the rightmost `@` and `semver.valid`s, with a digit-block fallback so `foo1.0.0`-style names still produce the existing "Did you mean foo?" hint. ### Bad code sanitization (8 alerts, test infrastructure) - `__utils__/test-ipc-server/src/TestIpcServer.ts`: the `JSON.stringify(...).slice(1, -1)` smell at the source of all 8 test-file alerts is gone. Both `sendLineScript` and `generateSendStdinScript` now build the JS source with plain `JSON.stringify` and delegate shell wrapping to a new `wrapNodeEval` helper that escapes `\\` and `"` for the outer double-quoted shell argument. ### Incomplete sanitization (2 alerts, test file) - `releasing/commands/test/publish/oidcProvenance.test.ts`: `.replace('/', '%2f')` → `.replaceAll(...)` on both flagged lines.
195 lines
5.9 KiB
TypeScript
195 lines
5.9 KiB
TypeScript
import { expect, test } from '@jest/globals'
|
|
import { guessDependencyType, updateProjectManifestObject } from '@pnpm/pkg-manifest.utils'
|
|
|
|
test('guessDependencyType()', () => {
|
|
expect(
|
|
guessDependencyType('foo', {
|
|
dependencies: {
|
|
bar: '1.0.0',
|
|
},
|
|
devDependencies: {
|
|
foo: '',
|
|
},
|
|
})
|
|
).toBe('devDependencies')
|
|
|
|
expect(
|
|
guessDependencyType('bar', {
|
|
dependencies: {
|
|
bar: '1.0.0',
|
|
},
|
|
devDependencies: {
|
|
foo: '1.0.0',
|
|
},
|
|
})
|
|
).toBe('dependencies')
|
|
})
|
|
|
|
test('peer dependencies fall back to "*" when resolved version is unavailable (git)', async () => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'https://github.com/kevva/is-negative',
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'https://github.com/kevva/is-negative',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: '*',
|
|
})
|
|
})
|
|
|
|
test('peer dependencies fall back to "*" when resolved version is unavailable (tarball)', async () => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'https://github.com/hegemonic/taffydb/tarball/master',
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'https://github.com/hegemonic/taffydb/tarball/master',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: '*',
|
|
})
|
|
})
|
|
|
|
test('peer dependencies use derived range when resolved version is available (git)', async () => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'https://github.com/kevva/is-negative',
|
|
resolvedVersion: '2.1.0',
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'https://github.com/kevva/is-negative',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: '^2.1.0',
|
|
})
|
|
})
|
|
|
|
test('peer dependencies honor pinned version when resolved version is available (tarball)', async () => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'https://github.com/hegemonic/taffydb/tarball/master',
|
|
resolvedVersion: '1.4.0',
|
|
pinnedVersion: 'minor',
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'https://github.com/hegemonic/taffydb/tarball/master',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: '~1.4.0',
|
|
})
|
|
})
|
|
|
|
test('peer dependencies derive range from resolved version for jsr protocol', async () => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'jsr:^0.1.0',
|
|
resolvedVersion: '0.1.0',
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'jsr:^0.1.0',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: '^0.1.0',
|
|
})
|
|
})
|
|
|
|
test('peer dependencies keep prerelease resolved version without prefix', async () => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'https://github.com/kevva/is-negative',
|
|
resolvedVersion: '2.1.0-rc.1',
|
|
pinnedVersion: 'minor',
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'https://github.com/kevva/is-negative',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: '2.1.0-rc.1',
|
|
})
|
|
})
|
|
|
|
test('writes prototype-conflicting aliases as own data properties without polluting Object.prototype', async () => {
|
|
const protoSnapshotBefore = Object.getOwnPropertyNames(Object.prototype).sort()
|
|
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{ alias: '__proto__', bareSpecifier: '1.0.0', saveType: 'dependencies' },
|
|
{ alias: 'constructor', bareSpecifier: '1.0.1', saveType: 'dependencies' },
|
|
{ alias: 'prototype', bareSpecifier: '1.0.2', saveType: 'dependencies' },
|
|
{ alias: 'real-pkg', bareSpecifier: '2.0.0', saveType: 'dependencies' },
|
|
])
|
|
|
|
// Each pollution-key alias is stored as a regular own data property.
|
|
const deps = manifest.dependencies!
|
|
expect(Object.hasOwn(deps, '__proto__')).toBe(true)
|
|
expect(Object.hasOwn(deps, 'constructor')).toBe(true)
|
|
expect(Object.hasOwn(deps, 'prototype')).toBe(true)
|
|
expect(Object.hasOwn(deps, 'real-pkg')).toBe(true)
|
|
// The own __proto__ data property shadows the inherited getter and returns the value.
|
|
expect(deps.__proto__).toBe('1.0.0')
|
|
expect(deps.constructor as unknown as string).toBe('1.0.1')
|
|
expect(deps.prototype as unknown as string).toBe('1.0.2')
|
|
// The prototype chain of `deps` is unchanged (the assignment did not run __proto__'s setter).
|
|
expect(Object.getPrototypeOf(deps)).toBe(Object.prototype)
|
|
|
|
// Object.prototype hasn't grown a new property.
|
|
expect(Object.getOwnPropertyNames(Object.prototype).sort()).toStrictEqual(protoSnapshotBefore)
|
|
})
|
|
|
|
test('peer dependencies respect pinned version "patch" and "none"', async () => {
|
|
const cases = [
|
|
{ pinnedVersion: 'patch' as const, expected: '3.2.1' },
|
|
{ pinnedVersion: 'none' as const, expected: '^3.2.1' },
|
|
]
|
|
|
|
await Promise.all(cases.map(async ({ pinnedVersion, expected }) => {
|
|
const manifest = await updateProjectManifestObject('/project', {}, [
|
|
{
|
|
alias: 'foo',
|
|
bareSpecifier: 'https://github.com/kevva/is-negative',
|
|
resolvedVersion: '3.2.1',
|
|
pinnedVersion,
|
|
peer: true,
|
|
saveType: 'devDependencies',
|
|
},
|
|
])
|
|
|
|
expect(manifest.devDependencies).toStrictEqual({
|
|
foo: 'https://github.com/kevva/is-negative',
|
|
})
|
|
expect(manifest.peerDependencies).toStrictEqual({
|
|
foo: expected,
|
|
})
|
|
}))
|
|
})
|