原文地址: How We're Protecting Our Newsroom from npm Supply Chain Attacks
本文并非原文的翻译,是经过我自己理解后的输出,聚焦我感兴趣或有价值的内容。对具体内容感兴趣的朋友可以去原文查看完整内容。
背景
2025 年 11 月,npm 供应链出现了蠕虫病毒攻击。很多基础且下载量较大的包都被感染,造成了非常严重的影响。而西雅图时报因为使用 pnpm 提供的安全控制功能,成功躲过了这次攻击。靠的不是什么高深的手段,而是把那段时间的依赖安装和升级“挡在门外”。
当时我们公司也因为这个事情紧急成立了安全小组,对项目中的 npm 依赖进行排查。尽管我们也没有受到影响,但让我们开始重视起安全这件事。对于大公司或者稳定的系统来说,安全与合规可能更重要。
客户端控制与 3 层纵深防线
npm 作为 JavaScript 生态最大的包管理工具,尽管其提供了一定程度的安全措施,但仍然属于发布端/平台的范围。作为使用者,也应该在客户端重视起来。
此前的供应链攻击,就是相关包作者被“钓鱼邮件”窃取了 npm 的账号,然后发布了带有恶意代码的依赖包,并在安装时利用 preinstall 或者 postinstall 钩子进行攻击。如果作为使用者,完全相信 npm 而没有一点防范意识,就很容易中招。
pnpm 则提供了一些安全控制功能,利用这些功能就可以建立起 3 层纵深防线。
1. 脚本生命周期管理(Lifecycle Script Management)
这次攻击最关键的一点,就是攻击者利用了 preinstall 和 postinstall 在安装依赖时植入恶意脚本。
pnpm 提供了 strictDepBuilds 选项,当设置为 true 时,会默认阻止依赖包执行构建脚本(除非显式允许)。你也可以通过配置,仅允许可信来源的包执行必要的脚本。
strictDepBuilds: true
onlyBuiltDependencies:
- package-with-necessary-build-scripts
ignoredBuiltDependencies:
- package-with-unnecessary-build-scripts
2. 延迟升级(Release Cooldown)
通常情况下,npm 上的包更新速度会比较快。如果没有使用 lock 文件去锁定依赖版本,那每次安装就很可能在不知道的情况下升级,就有可能引入风险。
pnpm 提供了 minimumReleaseAge 选项,相当于给了一个冷静期,比如设置为在最新版本发布后一周再允许升级。这样可以对新版本的稳定性进行一段时间的观察。对于成熟的系统来说,稳定性往往比追新更重要。同样地,也可以设置白名单,用来应对一些紧急的 CVE 修复补丁。
minimumReleaseAge: <duration-in-minutes>
minimumReleaseAgeExclude:
- package-with-critical-hotfix@1.2.3
3. 信任策略(Trust Policy)
这个特性也是我从这篇文章中第一次知道的。它的作用是当软件包的信任级别低于之前版本时,就会阻止安装。简单来说,如果一个包之前一直是通过 CI/CD 流程发布,这一次却突然变成了本地提交发布,那么系统就会怀疑作者凭据可能被窃取,从而阻止安装。
其原理是 npm 会有三个信任级别(从高到低):
- 受信发布者:通过 GitHub Actions 发布,带有 OIDC 和 npm provenance(出处证明)。
- 发布来源:来自 CI/CD 的签名证明。
- 非受信:使用用户名/密码或者 token 进行发布。
trustPolicy: no-downgrade
trustPolicyExclude:
- package-that-migrated-cicd@1.2.3
总结
安全从来不是单点防御,而是一套纵深体系。
pnpm 的这三项能力——脚本生命周期管理、延迟升级、信任策略——本质上是在三个关键点“把闸门关上”:安装时不轻易执行脚本、升级时给新版本留观察期、发布来源不允许突然降级。
这些策略可能会带来一点“麻烦”(比如需要维护白名单),但相比供应链攻击的破坏性,这点成本往往是值得的。而且这一套“纵深防线”的理念非常值得实践,执行起来也并不复杂:先从团队的核心项目开始试运行,逐步固化成默认配置,让安全不再依赖运气。
欢迎关注公众号:此方的手帐
个人博客: konata9.cc/
每周分享周刊/小知识,和你一起进步