Skip to main content

pnpm 11.4

· 10 min read
Zoltan Kochan
Lead maintainer of pnpm

pnpm 11.4 修补了围绕锁文件完整性、凭证范围、git 解析、补丁文件和依赖别名的一系列供应链漏洞,默认将 tar 包完整性不匹配视为严重安装失败(带有范围有限的 --update-checksums 选项可启用),并将 pnpm runtime set 的默认写入位置改为 devEngines.runtime,而不是 engines.runtime

🌐 pnpm 11.4 closes a cluster of supply-chain holes around lockfile integrity, credential scoping, git resolutions, patch files, and dependency aliases, makes tarball-integrity mismatches a hard install failure by default (with a narrowly-scoped --update-checksums opt-in), and changes pnpm runtime set to write to devEngines.runtime instead of engines.runtime by default.

小幅更改

🌐 Minor Changes

Tarball 完整性不匹配现在会导致严重错误

🌐 Tarball-integrity mismatches are now a hard failure

以前,当下载的 tar 包的哈希值与锁文件不匹配时,pnpm install(非冻结)会记录 ERR_PNPM_TARBALL_INTEGRITY,静默地从注册表重新解析,并覆盖锁定的完整性。因此,即使项目附带了已提交的锁文件,受损的注册表、代理或重新发布的版本也可能在干净的机器上替换为攻击者控制的内容。

🌐 Previously, pnpm install (non-frozen) would log ERR_PNPM_TARBALL_INTEGRITY when a downloaded tarball's hash didn't match the lockfile, silently re-resolve from the registry, and overwrite the locked integrity. A compromised registry, proxy, or republished version could therefore substitute attacker-controlled content on a clean machine even though the project shipped a committed lockfile.

pnpm install 现在以 ERR_PNPM_TARBALL_INTEGRITY 退出,并带有指向新的可选择标志的提示。

唯一的选择加入是 pnpm install --update-checksums —— 仅限于刷新注册表当前提供的锁定完整性值。它对应于 yarn 的同名标志。当绕过生效时仍会打印警告,因此该操作是可审计的。

🌐 The only opt-in is pnpm install --update-checksums — narrowly scoped to refreshing the locked integrity values from what the registry currently serves. It mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the operation is auditable.

--forcepnpm update 故意绕过完整性检查。它们是常规的刷新操作;在这些流程中静默覆盖被锁定的完整性会抹去已提交锁文件本应提供的保护。--frozen-lockfile 的行为保持不变。--fix-lockfile 保持其文档中描述的用途(填充缺失的锁文件条目),也不是绕过操作。

pnpm runtime set 默认会写给 devEngines.runtime

🌐 pnpm runtime set writes to devEngines.runtime by default

pnpm runtime set <name> <version> 现在默认将运行时保存到 devEngines.runtime,而不是 engines.runtime。传递 --save-prod(或 -P)可以改为保存到 engines.runtime。参见 #11948

补丁更改

🌐 Patch Changes

安全性:无范围凭证不再在注册表之间泄露

🌐 Security: unscoped credentials no longer leak across registries

在一个源中定义的未限定作用域的 _authToken(或 _auth,或 username + _password,或 tokenHelper)——~/.npmrc~/.config/pnpm/auth.ini、工作区 .npmrc、CLI 标志等——会作为 Authorization 头发送给不同(可能不受信任)源指定的任何注册表。相同的风险也扩展到客户端 TLS 凭证(certkey)。

🌐 An unscoped _authToken (or _auth, or username + _password, or tokenHelper) defined in one source — ~/.npmrc, ~/.config/pnpm/auth.ini, a workspace .npmrc, CLI flags, etc. — would be sent as an Authorization header to whichever registry a different (potentially untrusted) source named. The same exposure extended to client TLS credentials (cert, key).

pnpm 现在会在加载时将每个未加作用域的每个注册表设置(_authToken_authusername_passwordtokenHelpercertkey)重写为其 URL 作用域形式,使用相同来源中声明的 registry= 值(如果来源未声明,则使用 npmjs 默认注册表)。因此,后续覆盖层的 registry= 无法携带未加作用域的凭证,因为它已经固定为作者预期的 URL。ca / cafile 故意不重新作用域——它们是信任锚点,而非凭证,而且企业 MITM 代理设置依赖于它们在全局应用。

🌐 pnpm now rewrites each unscoped per-registry setting (_authToken, _auth, username, _password, tokenHelper, cert, key) to its URL-scoped form at load time, using the registry= value declared in the same source (or the npmjs default registry if the source declares none). A later layer overriding registry= therefore cannot pull an unscoped credential along, because it is already pinned to the URL its author intended. ca / cafile are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally.

每次重新定义作用域时,都会发出弃用警告,告诉用户该设置被固定在哪里以及如何直接编写它。自 npm@9 起,npm 已完全拒绝未加作用域的凭据,pnpm 打算在未来的主要版本中移除支持。要针对特定的注册表,请以 URL 作用域编写设置:

🌐 Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly. npm has rejected unscoped credentials outright since npm@9, and pnpm intends to remove support in a future major release. To target a specific registry, write the setting URL-scoped:

.npmrc
//registry.example.com/:_authToken=...
//registry.example.com/:cert=...

安全性:没有 integrity 的锁文件条目被拒绝

🌐 Security: lockfile entries without integrity are rejected

以前,当未提供完整性验证时,负责提取下载的 tar 包的工作程序会跳过哈希验证,并从未经验证的字节生成一个新的哈希。一个攻击者如果能够同时修改锁文件(例如,通过去掉 integrity: 的拉取请求)并在被引用的 tar 包 URL 上提供修改过的内容,就可以在没有任何错误的情况下安装被篡改的包——包括在 --frozen-lockfile 下。

🌐 Previously, the worker that extracts a downloaded tarball skipped hash verification when no integrity was supplied and minted a fresh one from the unverified bytes. An attacker who could both alter the lockfile (e.g. via a pull request that strips integrity:) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under --frozen-lockfile.

pnpm 现在在 lockfile 读取时以 ERR_PNPM_MISSING_TARBALL_INTEGRITY 失败。Git 托管的 tar 包(gitHosted: truecodeload.github.com / bitbucket.org / gitlab.com 上的 URL)和 file: tar 包不受影响 —— Git 托管 URL 中的提交 SHA 和用户控制的本地路径已经固定了字节内容。

🌐 pnpm now fails closed at lockfile-read time with ERR_PNPM_MISSING_TARBALL_INTEGRITY. Git-hosted tarballs (gitHosted: true or a URL on codeload.github.com / bitbucket.org / gitlab.com) and file: tarballs are exempt — the commit SHA in a git-host URL and the user-controlled local path already anchor the bytes.

安全性:git 解析拒绝非 SHA 的 commit 字段

🌐 Security: git resolutions reject non-SHA commit fields

Git 解析的 commit 字段如果不是 40 个字符的十六进制 SHA,会在调用 git 之前被拒绝。否则,恶意的锁文件可能通过 git fetch / git checkout 偷运例如 --upload-pack=<command> 的值,在 SSH 或本地文件传输时执行提供的命令。

🌐 Git resolutions whose commit field is not a 40-character hexadecimal SHA are rejected before git is invoked. A malicious lockfile could otherwise smuggle a value such as --upload-pack=<command> through git fetch / git checkout, which on SSH or local-file transports executes the supplied command.

安全性:拒绝在包目录外写入补丁文件

🌐 Security: patch files writing outside the package directory are rejected

现在会拒绝那些 diff --git 头引用补丁包目录之外路径的补丁文件。之前,通过拉取请求添加的恶意 .patch 文件可能会写入、删除或重命名由运行 pnpm install 的用户可访问的任意文件。

🌐 Patch files whose diff --git headers reference paths outside the patched package directory are now rejected. Previously a malicious .patch file added via a pull request could write, delete, or rename arbitrary files reachable by the user running pnpm install.

安全性:带有路径遍历段的依赖别名被拒绝

🌐 Security: dependency aliases with path-traversal segments are rejected

当从软件包清单中读取或通过符号链接链接到 node_modules 时,包含路径遍历段(例如 @x/../../../../../.git/hooks)的依赖别名会被拒绝。否则,恶意的注册表软件包可能会使用传递依赖键使 pnpm install 在攻击者选择的路径上创建符号链接,而这些路径位于预期的 node_modules 目录之外。

🌐 Dependency aliases that contain path-traversal segments (such as @x/../../../../../.git/hooks) are rejected when read from a package manifest or symlinked into node_modules. A malicious registry package could otherwise use a transitive dependency key to make pnpm install create symlinks at attacker-chosen paths outside the intended node_modules directory.

受信任发布者的元数据现在需要来源

🌐 Trusted-publisher metadata now requires provenance

只有在存在来源的情况下,可信发布者元数据才被视为最强的信任证据。

🌐 Trusted publisher metadata is only treated as the strongest trust evidence when provenance is also present.

其他修复

🌐 Other fixes

  • Fix pnpm deploy crashing with ENOENT: ... lstat '<deployDir>/node_modules' when configDependencies declares pacquet (pacquet or @pnpm/pacquet). The deploy directory never installs config dependencies, so the install engine they designate isn't on disk to invoke; the nested install now skips them.

  • Limit concurrent project manifest reads while listing large workspaces to avoid EMFILE errors.

  • Validate devEngines.runtime and engines.runtime version ranges for node, deno, and bun when onFail is set to error or warn. Previously these settings only had an effect with onFail: 'download' — the error and warn modes silently did nothing #11818. Violations now throw ERR_PNPM_BAD_RUNTIME_VERSION.

  • Improve the log message that pnpm prints after auto-adding entries to minimumReleaseAgeExclude when minimumReleaseAge is set without minimumReleaseAgeStrict. The message previously referred to the internal "loose mode" terminology, which wasn't searchable in the docs; it now tells the user to set minimumReleaseAgeStrict to true if they want these updates gated behind a prompt instead #11747.