mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 18:05:29 -04:00
Config dependency names and versions are read from the committed env lockfile (pnpm-lock.yaml) and the legacy inline-integrity format in pnpm-workspace.yaml, and both become path segments of the directories pnpm creates during install (node_modules/.pnpm-config/<name> and the global virtual store's <name>/<version>/<hash>). They were used unvalidated, so a malicious repository could commit a traversal-shaped name (../../PWNED) or version (../../../PWNED) and make `pnpm install` create symlinks or write package files outside those roots — triggered on install, even with --ignore-scripts. Add verifyEnvLockfile, an offline structural gate that validates every config dependency and optional-subdependency name (must be a valid npm package name) and version (must be an exact semver version) before any path is built from it. It runs at the install boundary and, through a single writeVerifiedEnvLockfile seam, before the env lockfile is ever persisted, so an invalid entry is rejected with no write side effect. __proto__ names are rejected too (the validation accumulators use null-prototype objects so the key can't slip past Object.keys). The same fix and structure land in pacquet to keep the two stacks in sync. Fixes GHSA-qrv3-253h-g69c.
107 lines
3.8 KiB
TypeScript
107 lines
3.8 KiB
TypeScript
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package'
|
|
import { writeSettings } from '@pnpm/config.writer'
|
|
import { PnpmError } from '@pnpm/error'
|
|
import { createEnvLockfile } from '@pnpm/lockfile.fs'
|
|
import { toLockfileResolution } from '@pnpm/lockfile.utils'
|
|
import type { ConfigDependencies, ConfigDependencySpecifiers, Registries } from '@pnpm/types'
|
|
import getNpmTarballUrl from 'get-npm-tarball-url'
|
|
|
|
import type { NormalizedConfigDep } from './parseIntegrity.js'
|
|
import { parseIntegrity } from './parseIntegrity.js'
|
|
import { writeVerifiedEnvLockfile } from './writeVerifiedEnvLockfile.js'
|
|
|
|
interface MigrateOpts {
|
|
registries: Registries
|
|
rootDir: string
|
|
}
|
|
|
|
/**
|
|
* Migrates old-format configDependencies (with inline integrity in pnpm-workspace.yaml)
|
|
* to the new pnpm-lock.yaml format.
|
|
*
|
|
* Returns normalized deps for immediate installation, and writes the env lockfile
|
|
* and clean specifiers to pnpm-workspace.yaml as a side effect.
|
|
*/
|
|
export async function migrateConfigDepsToLockfile (
|
|
configDeps: ConfigDependencies,
|
|
opts: MigrateOpts
|
|
): Promise<Record<string, NormalizedConfigDep>> {
|
|
const envLockfile = createEnvLockfile()
|
|
// Null-prototype so a `__proto__` name lands as an own key verifyEnvLockfile
|
|
// sees, not a silent prototype mutation.
|
|
envLockfile.importers['.'].configDependencies = Object.create(null)
|
|
const cleanSpecifiers: ConfigDependencySpecifiers = {}
|
|
const normalizedDeps: Record<string, NormalizedConfigDep> = {}
|
|
|
|
for (const [pkgName, pkgSpec] of Object.entries(configDeps)) {
|
|
const registry = pickRegistryForPackage(opts.registries, pkgName)
|
|
|
|
if (typeof pkgSpec === 'object') {
|
|
const { version, integrity } = parseIntegrity(pkgName, pkgSpec.integrity)
|
|
const tarball = pkgSpec.tarball ?? getNpmTarballUrl(pkgName, version, { registry })
|
|
|
|
cleanSpecifiers[pkgName] = version
|
|
const pkgKey = `${pkgName}@${version}`
|
|
envLockfile.importers['.'].configDependencies[pkgName] = {
|
|
specifier: version,
|
|
version,
|
|
}
|
|
envLockfile.packages[pkgKey] = {
|
|
resolution: toLockfileResolution(
|
|
{ name: pkgName, version },
|
|
{ integrity, tarball },
|
|
registry
|
|
),
|
|
}
|
|
envLockfile.snapshots[pkgKey] = {}
|
|
normalizedDeps[pkgName] = {
|
|
version,
|
|
resolution: { integrity, tarball },
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (typeof pkgSpec === 'string') {
|
|
// This branch only handles the legacy inline format (version+integrity).
|
|
// New clean specifiers (just version/range) require an existing pnpm-lock.yaml.
|
|
if (!pkgSpec.includes('+')) {
|
|
throw new PnpmError(
|
|
'CONFIG_DEP_MISSING_LOCKFILE',
|
|
`Config dependency "${pkgName}" is already in clean-specifier form (${pkgSpec}) ` +
|
|
'but no pnpm-lock.yaml was found to resolve it. ' +
|
|
'Please generate and commit pnpm-lock.yaml (for example by running ' +
|
|
'`pnpm install` in the workspace root) before attempting to migrate configDependencies.'
|
|
)
|
|
}
|
|
const { version, integrity } = parseIntegrity(pkgName, pkgSpec)
|
|
const tarball = getNpmTarballUrl(pkgName, version, { registry })
|
|
|
|
cleanSpecifiers[pkgName] = version
|
|
const pkgKey = `${pkgName}@${version}`
|
|
envLockfile.importers['.'].configDependencies[pkgName] = {
|
|
specifier: version,
|
|
version,
|
|
}
|
|
envLockfile.packages[pkgKey] = {
|
|
resolution: { integrity },
|
|
}
|
|
envLockfile.snapshots[pkgKey] = {}
|
|
normalizedDeps[pkgName] = {
|
|
version,
|
|
resolution: { integrity, tarball },
|
|
}
|
|
}
|
|
}
|
|
|
|
await writeVerifiedEnvLockfile(opts.rootDir, envLockfile)
|
|
await writeSettings({
|
|
rootProjectManifestDir: opts.rootDir,
|
|
workspaceDir: opts.rootDir,
|
|
updatedSettings: {
|
|
configDependencies: cleanSpecifiers,
|
|
},
|
|
})
|
|
|
|
return normalizedDeps
|
|
}
|