我们如何保护我们的新闻编辑部免受 npm 供应链攻击
我们在沙伊-胡卢德 2.0 上很幸运。
🌐 We got lucky with Shai-Hulud 2.0.
在2025年11月,一个自我复制的 npm 蠕虫攻击了796个软件包,这些软件包每月下载量达1.32亿次。该攻击利用 preinstall 脚本来窃取凭据、安装持久后门,并在某些情况下清除整个开发者环境。我们没有受到影响——并不是因为我们有强大的防御能力,而是因为在攻击期间我们没有运行 npm install 或 npm update。
🌐 In November 2025, a self-replicating npm worm compromised 796 packages with 132 million monthly downloads. The attack used preinstall scripts to steal credentials, install persistent backdoors, and in some cases wipe entire developer environments. We weren't affected—not because we had robust defenses, but because we didn't run npm install or npm update during the attack window.
运气不是一种安全策略。
🌐 Luck isn't a security strategy.
我们是谁
🌐 Who We Are
我是瑞安·索博尔,西雅图时报的首席软件工程师。多年来,我们一直使用 npm 作为默认的包管理器,也曾短暂尝试过 Yarn,但从未获得广泛应用。现在,我们正在试点使用 pnpm,特别是因为它的客户端安全控制可以补充 npm 一直在推出的注册表级改进。
🌐 I'm Ryan Sobol, Principal Software Engineer at the Seattle Times. We've been using npm as our default package manager for years, with some brief experimentation with Yarn that never gained traction. Now we're piloting pnpm specifically for its client-side security controls that complement the registry-level improvements npm has been rolling out.
信任对新闻机构至关重要,尤其是在当下。供应链受损可能会泄露客户数据、员工凭证、生产基础设施以及源代码——这些都可能需要数周时间来恢复,并可能要求我们向读者发出安全泄露通知。我们明白这些事件在时间和金钱上都可能非常昂贵。这是我们不想走的路。
🌐 Trust is paramount for news organizations, especially these days. A supply chain compromise could expose customer data, employee credentials, production infrastructure, and source code—all things that could take weeks to recover from and potentially require breach notifications to our readers. We understand how expensive these incidents can be in both time and money. That's a path we don't want to go down.
尽管坚持使用 npm 会带来组织惯性,我们认为 pnpm 在这里确实有机会。它是真正的即插即用替代方案——相同的命令、相同的工作流程、相同的注册表。这使得迁移变得可实现,而这是之前的替代方案无法做到的。
🌐 Despite the organizational inertia that comes with sticking to npm, we think pnpm has a real chance here. It's a true drop-in replacement—same commands, same workflows, same registry. That makes the transition achievable in a way previous alternatives weren't.
这不是一个精心准备的案例研究。这是来自一个刚开始了解供应链安全的团队的真实世界数据点。我们遇到的挑战以及我们对这些控制措施的思考,可能在你考虑自己实现它们时有所帮助。
🌐 This isn't a polished case study. It's a real-world data point from a team that's just starting to figure out supply chain security. The challenges we're encountering and how we're thinking about these controls might be useful as you consider implementing them yourself.
为什么客户端控件很重要
🌐 Why Client-Side Controls Matter
npm 在供应链安全方面取得了巨大的进展。受信任的发布、来源证明 和 细粒度访问令牌 都是重要的改进,使得在维护者账户被入侵后发布恶意软件包变得更加困难。
🌐 npm has made tremendous progress on supply chain security. Trusted publishing, provenance attestations, and granular access tokens are all significant improvements that make it substantially harder to publish malicious packages after compromising maintainer accounts.
但这里有个漏洞:这些注册表改进保护的是发布方。它们并不能阻止使用恶意软件包的人。
🌐 But here's the gap: these registry improvements protect the publishing side. They don't prevent consuming malicious packages.
当你运行 npm install 或 npm update 时,生命周期脚本(例如 preinstall 和 postinstall)会在包被评估安全性之前,以完全的开发者权限执行来自互联网的任意代码。这些脚本可以访问你的凭据(npm、GitHub、AWS、数据库)、你的源代码、你的云基础设施,以及你的整个文件系统。
🌐 When you run npm install or npm update, lifecycle scripts (e.g., preinstall and postinstall) execute arbitrary code from the internet with full developer privileges—before the package has been evaluated for safety. These scripts can access your credentials (npm, GitHub, AWS, databases), your source code, your cloud infrastructure, and your entire filesystem.
这是Shai-Hulud等攻击所利用的基本漏洞。即使有了这些注册表改进,如果合法维护者的账户被攻破,攻击者仍然可以发布带有恶意生命周期脚本的版本,这些脚本会在安装时立即执行——在社区发现被攻破之前。
🌐 This is the fundamental vulnerability that attacks like Shai-Hulud exploit. Even with these registry improvements, if a legitimate maintainer's account is compromised, attackers can publish a version with malicious lifecycle scripts that execute immediately upon installation—before the community detects the compromise.
这就是为什么我们认为需要在两方面进行防护:npm 的改进使得发布恶意包更困难;pnpm 的客户端控制使得使用它们更困难。这些方法是互补的,而不是竞争的。pnpm 使用 npm 的注册表,并从 npm 的所有安全改进中受益,同时在客户端增加了一层额外的保护。
🌐 That's why we felt we needed defense on both sides: npm's improvements make it harder to publish malicious packages; pnpm's client-side controls make it harder to consume them. These approaches are complementary, not competitive. pnpm uses npm's registry and benefits from all of npm's security improvements while adding an additional layer of protection on the client side.
这是纵深防御。
🌐 This is defense-in-depth.
我们使用的三层
🌐 The Three Layers We're Using
在我们的试点项目中,我们使用了三种协同工作的 pnpm 安全控制。每种控制针对不同的攻击向量,并且每种控制都有合法例外的逃生通道。我们一开始就知道我们会需要这些例外——现实世界非常复杂。
🌐 For our pilot, we're using three pnpm security controls that work together. Each control addresses a different attack vector, and each has escape hatches for legitimate exceptions. We knew going in that we'd need those exceptions—the real world is messy.
控制 1:生命周期脚本管理
🌐 Control 1: Lifecycle Script Management
我们考虑 pnpm 的主要原因之一是了解到它默认会阻止生命周期脚本。与其他包管理器不同,它不会隐式地信任并执行来自包的任意代码。
🌐 One of the main reasons we considered pnpm was learning that it blocks lifecycle scripts by default. Unlike other package managers, it doesn't implicitly trust and execute arbitrary code from packages.
在实践中,当一个包有 preinstall 或 postinstall 脚本时,pnpm 会阻止它们,但安装会继续,并伴随一个警告。这已经提供了显著的保护——恶意脚本不会在没有你明确允许的情况下执行。然而,我们担心警告可能太容易被忽视,尤其是因为安装看起来已经成功。我们希望通过 strictDepBuilds: true 实现更严格的控制:
🌐 In practice, when a package has preinstall or postinstall scripts, pnpm blocks them but installation continues with a warning. This already provides significant protection—malicious scripts won't execute without you explicitly allowing them. However, we were concerned that warnings would be too easy to ignore, especially since installation appears to succeed. We wanted stricter control with strictDepBuilds: true:
strictDepBuilds: true
onlyBuiltDependencies:
- package-with-necessary-build-scripts
ignoredBuiltDependencies:
- package-with-unnecessary-build-scripts
我们所说的“必要”,是指那些确实需要其生命周期脚本才能正常运行的包——例如需要从源码编译的本地扩展或需要链接特定平台库的数据库驱动程序。我们所说的“非必要”,是指可选的优化或设置步骤,这些步骤不会影响包在我们的使用场景中的功能。
🌐 By "necessary," we mean packages that genuinely need their lifecycle scripts to function—things like native extensions that compile from source or database drivers that link against platform-specific libraries. By "unnecessary," we mean scripts that are optional optimizations or setup steps that don't affect whether the package functions in our use case.
使用 strictDepBuilds: true 时,安装在遇到生命周期脚本时会立即失败,迫使我们必须:
🌐 With strictDepBuilds: true, installation fails immediately when it encounters lifecycle scripts, forcing us to:
- 识别哪些包有生命周期脚本——pnpm 会准确告诉你哪些有
- 研究每个脚本的作用,这可以像将自包含的预安装或后安装脚本输入生成式人工智能进行解释一样简单
- 使用人工判断对是否允许或阻止它做出有意识的、记录在案的决定
对于我们的团队来说,这确保了我们在一开始就做出深思熟虑的选择,而不是可能在之后才发现问题。
🌐 For our team, this ensures we're making deliberate choices upfront rather than potentially discovering issues later.
注意: pnpm 团队正在考虑在 v11 中将 strictDepBuilds: true 设置为默认行为,同时也在根据实际执行这些控制的团队的反馈,探索更清晰的 allow/deny 语法命名方式。
控制 2:释放冷却
🌐 Control 2: Release Cooldown
此控制会阻止在冷却期内发布的包版本的安装。其目的是为社区提供时间在恶意包到达你的环境之前检测并移除它们。
🌐 This control blocks installation of package versions published within a cooldown period. The idea is to give the community time to detect and remove malicious packages before they reach your environment.
minimumReleaseAge: <duration-in-minutes>
minimumReleaseAgeExclude:
- package-with-critical-hotfix@1.2.3
我们的思维转变: 我们不得不重新训练自己,停止认为“最新的就是最好的”。我们正在学习的是,从供应链安全的角度来看,情况并非总是如此——稍微旧一些的版本往往更安全。已经发布一段时间的软件包可以给社区和安全研究人员时间来发现潜在问题。
查看最近的攻击情况,恶意软件包已在不同的时间范围内被检测并移除。2025年9月的npm供应链攻击涉及到debug、chalk和其他16个软件包,被移除大约用了2.5小时,而Shai-Hulud 2.0(2025年11月)则大约用了12小时。每次攻击都不同,每次恢复的时间线也会有所不同,但适当的冷却期取决于贵组织的风险承受能力——可以以小时、天或周为单位。无论哪种方式,冷却期本可以阻止这些攻击。
🌐 Looking at recent attacks, malicious packages have been detected and removed in varying timeframes. The September 2025 npm supply chain attack that compromised debug, chalk, and 16 other packages saw removal within about 2.5 hours, while Shai-Hulud 2.0 (November 2025) took about 12 hours. Every attack is different and every recovery timeline will vary, but the appropriate cooldown period depends on your organization's risk tolerance—it could be measured in hours, days, or weeks. Either way, a cooldown period would have blocked these attacks.
我们接受的权衡: 鉴于我们组织的规模和优先事项,尽管我们尽了最大努力,我们并不总是使用软件包的最新版本。因此,这项冷却策略与我们的实际情况更为一致,而非干扰。当我们确实需要更新的版本(关键安全补丁、严重错误)时,经过审核后我们可以暂时豁免。
控制 3:信任策略
🌐 Control 3: Trust Policy
当软件包版本的认证强度比之前发布的版本弱时,此控制会阻止安装——这通常是攻击者入侵维护者凭证并使用自己的机器而不是官方 CI/CD 管道进行发布的迹象。
🌐 This control blocks installation when a package version has weaker authentication than previously published versions—often a sign that an attacker compromised maintainer credentials and published from their own machine instead of the official CI/CD pipeline.
trustPolicy: no-downgrade
trustPolicyExclude:
- package-that-migrated-cicd@1.2.3
工作原理: npm 会跟踪已发布包的三个信任级别(从最强到最弱):
- 可信发布者: 通过 GitHub Actions 使用 OIDC 令牌和 npm 溯源发布
- 来源: 来自 CI/CD 系统的签名证明
- 无信任证据: 使用用户名/密码或令牌认证发布
如果较新版本的认证比旧版本弱,安装将失败。例如,如果 v1.0.0 是以受信任的发布者身份发布的,但 v1.0.1 是以基本认证发布的,pnpm 会阻止 v1.0.1。
🌐 If a newer version has weaker authentication than an older version, installation fails. For example, if v1.0.0 was published with Trusted Publisher but v1.0.1 was published with basic auth, pnpm blocks v1.0.1.
在2025年8月的s1ngularity攻击中,攻击者入侵了维护者的凭据,并从他们自己的机器发布了恶意版本。由于他们没有CI/CD访问权限,恶意版本没有来源——这是一个明显的信任下降。该控制本可以阻止安装。
🌐 In the s1ngularity attack in August 2025, attackers compromised maintainer credentials and published malicious versions from their own machines. Because they didn't have CI/CD access, the malicious versions had no provenance—a clear trust downgrade. This control would have blocked installation.
当信任度降低可能是合理的情况: 新维护者尚未设置来源信息,CI/CD 系统迁移,或在 CI/CD 停止工作时手动发布的紧急修复。在这些情况下,我们会调查信任度下降的原因,确认其安全性,然后添加到 trustPolicyExclude。
注意: 此功能于 2025 年 11 月被添加到 pnpm 中,属于相当新的功能。我们仍在了解在实际操作中合法的信任降级发生的频率。
它们如何协作:React 示例
🌐 How They Work Together: The React Example
我们不认为这些控制措施是灵丹妙药。它们作为防御层起作用——当我们需要对某一项控制措施作出例外时,其他层仍会继续保护我们。
🌐 We don't see any of these controls as a silver bullet. They work as layers of defense—when we need to make an exception for one control, the other layers continue protecting us.
让我们来看看一个实际的场景:2025年12月披露的关键React漏洞。
🌐 Let's look at a real scenario: the critical React vulnerability disclosed in December 2025.
这是一个严重的安全问题,需要立即修补。通常,我们的发布冷却期会阻止我们安装刚发布的包版本。但这是一个关键的安全补丁——我们不能等待。
🌐 This was a serious security issue that required immediate patching. Normally, our release cooldown would prevent us from installing a package version published so recently. But this was a critical security patch—we couldn't wait.
在这种情况下,分层防御将这样运作:
🌐 Here's how the layered defense would work in this scenario:
你将要做的事情: 在查看漏洞披露并验证补丁合法后,将特定的 React 版本添加到 minimumReleaseAgeExclude。
仍然保护你的事物:
-
Lifecycle Script Management is still active—if an attacker had injected malicious lifecycle scripts into the React patch, they would be blocked (React normally has no lifecycle scripts, so any scripts would be immediately suspicious)
-
Trust Policy is still active—if an attacker had compromised React's publishing credentials and pushed a malicious "patch" from their own machine, the trust downgrade would be blocked
这就是为什么我们认为例外是可以预期且可以接受的原因。你会出于合理的原因,做出有意识的、经过记录的决定来绕过一项控制,但你仍然可以从其他层获得强有力的保护。没有单点故障。
🌐 This is why we think exceptions are expected and okay. You make a conscious, documented decision to bypass one control for a legitimate reason, but you still have robust protection from the other layers. No single point of failure.
这就是对我们来说的纵深防御在实践中的样子。
🌐 This is what defense-in-depth looks like in practice for us.
我们的试点体验
🌐 Our Pilot Experience
我们在一个后端服务中实现了所有三项安全控制,作为概念验证。总设置时间:几小时,用于研究、理解和定义我们的方案。
🌐 We implemented all three security controls in one of our backend services as a proof of concept. Total setup time: a few hours to research, understand, and define our approach.
在设置过程中,pnpm 识别了三个具有生命周期脚本的包:
🌐 During setup, pnpm identified three packages with lifecycle scripts:
-
esbuild: Optimizes CLI tool startup by milliseconds—not needed since we only use the JavaScript API
-
@firebase/util: Auto-configures client SDK—not needed since we only use the server SDK
-
protobufjs: Checks version schema compatibility—not needed since it's a transitive dependency
我们研究了每个脚本的功能(阅读文档并将脚本输入AI进行解读),确定这些脚本对于我们的使用场景都不必要,于是将它们屏蔽了。对功能没有影响。
🌐 We researched what each script did (reading documentation and feeding the scripts to AI for interpretation), determined none were necessary for our use case, and blocked them. Zero impact on functionality.
就是这样。几个小时的初始投入,就能获得对沙丘虫式攻击的持续防护。
🌐 That was it. A few hours of initial investment for ongoing protection against Shai-Hulud-style attacks.
摩擦的感觉: 这些控制的设计本身就会产生摩擦——对我们来说,这是一个特性,而不是一个缺陷。摩擦迫使我们有意识地决定在我们的环境中运行哪些代码,而不是默认信任所有内容。当新的依赖包含脚本时,我们预计大约需要 15 分钟来审核并记录决策。
我们预计随着对这个过程的熟悉,摩擦会随着练习变得更加直观。
🌐 We expect that the friction will become more intuitive with practice as we get more familiar with the process.
我们正在学习的内容
🌐 What We're Learning
我们从试点中学到的一些事情:
🌐 A few things we've learned from our pilot:
纵深防御模型实际上是有效的。 在客户端设置多层防护——再加上 npm 发布端改进所带来的好处——意味着我们可以对例外情况采取务实的态度。当我们因为正当理由需要绕过某个控制措施时,其他措施仍然在保护我们。这消除了对例外的焦虑——它们不是安全失败,而是系统按设计运作的体现。
这种思维模式需要时间。 从“以便利为先”转向“以安全为先”有一个学习曲线。但一旦这种思维模式理顺了——稍微老一点的包更安全,明确的决策比隐含的信任更好——工作流程就会感觉很自然。
这些控制措施适用于中型团队。 我们不是拥有专门安全团队的大型科技公司。我们是一家中型新闻媒体机构,工程资源有限。如果我们能成功实现这些控制措施,它们对大多数团队都是可行的。
我们仍在学习。 威胁环境在不断变化,我们的方法也会随之调整。信任策略功能才推出几周时间,我们尚不清楚在实际操作中合法的信任降级会发生频繁。我们计划在不久的将来将这些控制扩展到其他代码库,这将为我们提供更多关于它们如何在具有不同依赖图的应用中扩展的数据。
对于考虑这一点的其他团队
🌐 For Other Teams Considering This
如果你正在考虑 pnpm 的安全控制,以下是我们成功的做法:
🌐 If you're considering pnpm's security controls, here's what worked for us:
从一个项目开始。 先在一个代码库上试点,让我们熟悉工作流程,了解摩擦点,并在考虑更广泛的推广之前建立信心。
提前为例外情况做计划。 在开始时就要预期你需要为生命周期脚本(需要编译的包)、发布冷却期(关键安全补丁)以及信任降级(CI/CD 迁移)准备例外情况。这不是失败——这是系统设计运作的方式。
从第一天起就使用 strictDepBuilds: true。 仅仅依赖警告对我们来说风险太大。我们希望安装立即失败,从而强制做出决定。这可以防止软件包以后可能出现异常行为,并确保每一步都是经过深思熟虑的选择。
记录每一个例外。 写下你为什么允许某个生命周期脚本或豁免某个软件包。这会创建审计记录,帮助未来的团队成员理解原因,并且使日后清理例外情况变得容易。
相信层次。 当你对某一个控制措施做出例外时,请记住另外两个仍在保护你。纵深防御模型让你有空间采取务实的做法。
分享你的经验
🌐 Share Your Experience
我们很想听听其他正在实现这些控制措施或正在考虑实现的团队的意见。什么方法有效?什么具有挑战性?你学到了什么?加入 pnpm GitHub 讨论 的对话,或者在社交媒体上分享你的经验——我们都在一起学习。
🌐 We'd love to hear from other teams implementing these controls or considering them. What's working? What's challenging? What have you learned? Join the conversation in the pnpm GitHub Discussions or share your experiences on social media—we're all learning together.
谢谢
🌐 Thank You
感谢 pnpm 团队开发这些控件,并以深思熟虑的方式让它们既强大又实用。也感谢你们邀请我们分享我们的故事。
🌐 Thanks to the pnpm team for building these controls and for the thoughtful way they've approached making them both powerful and practical. And thanks for inviting us to share our story.
你正在做的工作很重要。这些控制措施提供了真正的保护,与 npm 的注册表改进相辅相成。它们共同为像我们这样的团队在对抗日益复杂的供应链攻击时提供了一线生机。
🌐 The work you're doing matters. These controls provide real protection that complements npm's registry improvements. Together, they give teams like ours a fighting chance against increasingly sophisticated supply chain attacks.
Ryan Sobol 是《西雅图时报》的首席软件工程师,他从事移动和网页开发、云基础设施以及开发者工具的工作。此处表达的观点为其个人观点,并基于《西雅图时报》对 pnpm 安全控制的试点实现。