Shai-Hulud供应链攻击:Keyv等npm包遭入侵详解与防御指南

事件概述
开源生态系统再次遭遇严重的供应链安全危机。近日,安全社区披露,广受欢迎的 npm 包 Keyv 及其一系列关联依赖包在一场名为 Shai-Hulud 的活跃供应链攻击中被攻陷。这一消息迅速引发了开发者社区的高度警惕,因为 Keyv 作为一个通用的键值存储抽象层,被大量项目直接或间接依赖。
Shai-Hulud(名称取自弗兰克·赫伯特的科幻巨著《沙丘》系列中的巨型沙虫,在小说中被弗里曼人视为沙漠之神的化身)是近期活跃的一类自我传播型 npm 攻击的代号。这类攻击的核心特征在于其蠕虫式的扩散能力——一旦某个维护者的账户或发布凭证被窃取,攻击者便会向其维护的多个包中注入恶意代码,进而利用窃取到的更多凭证继续横向传播,如同沙丘中不断吞噬一切的沙虫。安全研究人员选择这一命名,恰如其分地描绘了攻击在依赖生态中"地下潜行、突然爆发"的行为模式。
值得注意的是,Shai-Hulud 在《沙丘》中不仅是巨型沙虫的名称,更代表一种生态循环——沙虫产生香料,香料驱动星际文明,文明依赖香料而无法摆脱对沙虫的依附。这一隐喻精确对应了 npm 生态的现实:开发者依赖开源包提高效率,这种依赖创造了攻击者可利用的信任网络,而这种信任网络的存在又源于开发者对效率的追求。攻击者选择这一命名,暗示了他们对生态系统结构性脆弱的深刻理解。

为什么 Keyv 成为供应链攻击的核心目标
Keyv 在 npm 生态中的关键地位
Keyv 是一个简单但功能强大的键值存储库,它提供了统一的 API,可以对接 Redis、MongoDB、SQLite、内存缓存等多种后端。正因其灵活性和易用性,Keyv 被众多知名框架和工具链纳入依赖树中,包括缓存库、HTTP 客户端(如 got)等。
要理解 Keyv 的影响范围,需要了解 npm 生态中**传递依赖(transitive dependency)**的概念。传递依赖是指项目并未直接声明,但通过直接依赖的依赖链间接引入的包。在 npm 生态中,一个典型的中等规模项目可能声明了 30-50 个直接依赖,但其完整的依赖树往往包含 500-1500 个包。据 npm 官方数据,注册表中托管超过 200 万个包,每周下载量超过数十亿次。在这种高度互联的网络中,如果从图论的角度分析 npm 依赖图,Keyv 属于"高介数中心性(high betweenness centrality)"节点——大量最短路径经过它,意味着它的失陷能以指数级速度影响下游。研究表明,npm 生态中排名前 1% 的高连接度包可以直接或间接影响超过 70% 的活跃项目。
从网络科学的视角来看,npm 依赖图是一个典型的无标度网络(scale-free network),符合幂律分布——少数节点拥有极高的连接度,而大多数节点连接稀疏。这种网络结构在 Albert-Barabási 模型中被证明对随机故障具有鲁棒性,但对针对性攻击(即定向攻击高连接度节点)极为脆弱。2019 年一项针对 npm 的学术研究表明,仅移除 20 个关键包就能使超过 60% 的生态系统功能受损。Keyv 正处于这种拓扑结构中的关键位置。
这意味着即便开发者从未主动安装过 Keyv,他们的项目也极有可能通过传递依赖引入了这个包。这种深度嵌入生态的特性,正是供应链攻击最青睐的目标——攻击一个高连接度的节点,就能以最小成本影响海量下游项目。
攻击的连锁效应与扩散范围
所谓 "Keyv and friends",指的正是与 Keyv 相关的一整组包。攻击者往往不满足于攻陷单一目标,而是利用被入侵账户的发布权限,同时污染多个相关联的包。这种批量投毒的方式极大地扩大了攻击面,也让防御和清理工作变得异常复杂。
在 npm 的权限模型中,一个维护者账户通常拥有对多个包的发布权限。许多开源维护者同时管理着一组功能相关的包(例如 Keyv 的各种存储适配器:@keyv/redis、@keyv/mongo、@keyv/sqlite 等)。一旦单个账户被攻破,攻击者便获得了对整个包族的控制权。
npm 早期采用极其宽松的权限模型——任何拥有发布权限的维护者都可以随时发布任意版本,无需代码审查或二次确认。这种设计源于 npm 诞生时期(2010 年)的社区文化:小规模、高信任、快速迭代。随着生态膨胀到 200 万+ 包,这种信任模型已明显不适应当前的威胁态势。npm 在 2023 年引入的细粒度令牌和发布来源证明是对这一历史债务的偿还,但大量存量包仍使用传统的全权限令牌。
更棘手的是,npm 的版本发布是即时生效的——新版本发布后,所有未锁定精确版本的下游项目在下次安装时就会自动拉取恶意版本,这个时间窗口往往只有数小时,但已足够造成大规模感染。
Shai-Hulud 攻击的技术手法解析
蠕虫式自我传播机制
Shai-Hulud 系列攻击最令人担忧的特征是其自动化的传播能力。恶意代码通常会在包安装或运行时执行 postinstall 等生命周期脚本,扫描本地环境中的敏感信息。
npm 的**生命周期脚本(lifecycle scripts)**是 package.json 中定义的一系列钩子函数,它们在包安装的不同阶段自动执行。其中 preinstall、install、postinstall 三个钩子最为关键:preinstall 在包安装前执行,postinstall 在安装完成后执行。这些脚本拥有与当前用户相同的系统权限,可以执行任意代码——读写文件、发起网络请求、启动子进程。这一设计初衷是为了支持原生模块编译等合法用途(如 node-gyp 编译 C++ 扩展),但也为恶意代码提供了完美的执行入口。
这里体现了软件工程中的一个经典悖论:功能性需求与安全性需求的根本冲突。原生 C++ 扩展(如 node-sass、bcrypt、canvas 等)确实需要在安装时执行编译命令,而 WebAssembly 虽然提供了部分替代方案,但在性能关键场景下仍无法完全取代原生编译。Deno 和 Bun 等新兴运行时选择默认禁止安装脚本执行,通过显式权限授予来解决这一问题,但这也牺牲了与现有 npm 生态的无缝兼容性。
攻击载荷扫描的敏感信息通常包括:
- npm 的认证令牌:存储在
.npmrc文件中的 token。.npmrc是 npm 的配置文件,位于用户主目录或项目根目录,其中的//registry.npmjs.org/:_authToken=字段存储了用于身份认证的令牌。拥有此令牌即可以该用户的身份发布包,无需额外验证(除非启用了 2FA)。 - 云服务凭证:AWS 的
~/.aws/credentials文件、GCP 的服务账号 JSON 密钥、Azure 的订阅凭证等。 - 环境变量中的密码与 API Key:许多开发者将敏感信息存储在环境变量中,而这些变量对任何同用户运行的进程都是可读的。
- CI/CD 系统中的部署密钥:在 GitHub Actions、GitLab CI、Jenkins 等持续集成环境中,构建任务通常需要访问 npm 发布令牌和部署密钥。这些密钥虽然以"secrets"形式注入,但在运行时会解密为环境变量,恶意的
postinstall脚本完全可以在构建阶段读取它们。
CI/CD 环境之所以成为供应链攻击的首选目标,是因为它同时满足三个条件:拥有高权限凭证(发布令牌、部署密钥、云服务凭证)、自动化执行无人监督、且通常具有外网访问能力。2023 年 CircleCI 安全事件中,攻击者通过单个被入侵的工程师笔记本,获取了存储在 CircleCI 平台中的数千个客户 secrets。GitHub Actions 的 GITHUB_TOKEN 虽然会在工作流结束后自动过期,但在工作流运行期间(可能持续数十分钟)仍然是可被恶意脚本读取的。
一旦获取到新的 npm 发布权限,恶意脚本便会自动向受害者维护的其他包中植入相同的载荷,形成蠕虫式的链式感染。这也是它被冠以"沙虫"之名的原因。这种自我复制机制使得攻击的传播速度远超人工响应速度——在安全团队发现并撤回第一个恶意版本之前,病毒可能已经扩散到数十个新的包中。
凭证窃取与数据外泄路径
除了自我复制,攻击载荷通常还会将窃取到的敏感数据外泄至攻击者控制的服务器,或利用 GitHub 等公开平台创建仓库来暂存被盗信息。
数据外泄(data exfiltration)的技术手段多种多样,攻击者会根据目标环境的网络限制选择不同策略:最常见的是通过 HTTPS POST 请求将数据发送到攻击者控制的远程服务器,这种流量与正常的 API 调用无异,极难被防火墙识别;更隐蔽的手法包括 DNS 隧道(DNS tunneling)——将窃取的数据编码到 DNS 查询的子域名中,由于 DNS 流量几乎不会被企业网络拦截,这种方式的隐蔽性极高;还有攻击者利用 GitHub/GitLab 的公开 API 创建私有仓库或 Gist 来存储被盗数据,由于这些平台的域名在大多数网络环境中都是白名单,流量不会触发任何安全告警。
DNS 隧道技术利用了 DNS 协议的递归查询机制:当本地 DNS 服务器无法解析一个域名时,会逐级向上查询直至到达该域名的权威 DNS 服务器。攻击者注册一个域名(如 evil.com)并控制其权威 DNS 服务器,然后将窃取的数据编码为子域名查询(如 base64encodeddata.evil.com)。每次 DNS 查询可携带约 250 字节数据,虽然带宽极低,但足以传输 API 密钥和令牌。企业 DLP(数据防泄漏)系统通常不会深度检测 DNS 流量,使这种技术的检出率极低。
此前在 ua-parser-js、coa、rc 等包的供应链攻击中,安全研究人员已多次观察到类似的外泄模式。攻击者甚至会在载荷中加入延时执行和条件判断逻辑——仅在检测到 CI 环境(通过 CI=true 环境变量)时才激活凭证窃取行为,以降低在开发者本地机器上被发现的概率。
开发者应急响应与排查步骤
立即排查与修复措施
对于可能受影响的开发者和团队,建议采取以下措施:
- 锁定依赖版本:检查
package-lock.json或yarn.lock,确认 Keyv 及相关包是否被更新到可疑的恶意版本。锁文件(lockfile)记录了依赖树中每个包的精确版本号和完整性哈希(integrity hash),这是防止自动升级到恶意版本的第一道防线。务必将锁文件纳入版本控制。 - 审计安装脚本:警惕包中异常的
postinstall脚本行为,可通过--ignore-scripts参数进行防御性安装。还可以使用npm query ':attr(scripts, [postinstall])'命令列出所有包含 postinstall 脚本的依赖。 - 轮换所有凭证:如果曾在受影响时间窗口内安装过相关包,应立即轮换 npm token、云密钥及 CI/CD 凭证。npm 令牌可通过
npm token revoke命令撤销,随后重新生成。同时检查 npm 的访问日志,确认是否有异常的包发布行为。 - 回滚到已知安全版本:将依赖固定到攻击发生前的可信版本,并等待官方发布修复。可使用
npm audit signatures验证包的签名完整性。
长期供应链安全防御策略
这起事件再次凸显了现代软件供应链的脆弱性。团队应建立更完善的防护体系:
-
启用 npm 的双因素认证(2FA),并强制包发布使用 provenance(来源证明)。npm provenance 是基于 Sigstore 项目构建的供应链安全机制。Sigstore 是 Linux 基金会下的开源项目,提供无密钥签名(keyless signing)能力——它将包的发布与特定的 CI/CD 工作流绑定,通过 OIDC(OpenID Connect)协议验证发布者身份,并将签名记录到不可篡改的透明日志(Rekor)中。启用 provenance 后,用户可以验证一个包是否确实由其声明的 GitHub Actions 工作流构建和发布,而非被盗用令牌手动推送。
与传统的代码签名相比,Sigstore 的革新在于引入了「无密钥签名」概念:签名者通过 OIDC 身份提供商(如 GitHub、Google)进行身份验证,Sigstore 的证书颁发机构 Fulcio 为其颁发一个仅有效 10 分钟的短期证书,签名完成后私钥即被销毁。签名事件被记录到 Rekor 透明日志中,任何人都可以验证。这种设计将密钥管理的复杂性从维护者转移到了基础设施层,大幅降低了采用门槛——维护者无需管理长期私钥,也无需担心私钥泄露。
-
使用 Socket、Snyk 等供应链安全工具对依赖进行持续监控。这类工具的工作原理各有侧重:Socket 采用"深度包检测"策略,静态分析包的源代码以识别网络请求、文件系统访问、环境变量读取等可疑行为模式,而非仅依赖已知漏洞数据库;Snyk 则维护着一个持续更新的漏洞数据库,结合依赖图分析评估实际可达性(reachability)。两者结合可以覆盖"已知漏洞"和"零日恶意行为"两类威胁。
-
在 CI 环境中隔离敏感凭证,遵循最小权限原则。具体做法包括:为 npm 发布使用限制范围的细粒度令牌(granular access tokens)而非全局令牌;在 CI 中将包安装和包发布分离为不同的作业(jobs),安装阶段不暴露发布凭证;使用 OIDC 联合身份认证替代长期有效的静态令牌。
-
定期审查依赖树,减少不必要的传递依赖。可使用
npm ls --all查看完整依赖树,评估是否存在可替代的轻量方案。社区中也兴起了"零依赖"运动,提倡对于简单功能直接实现而非引入外部包。
反思:开源信任链的持续危机
Shai-Hulud 攻击并非孤例。从早年的 event-stream 事件,到近年针对多个热门包的持续投毒,npm 生态的信任模型正面临严峻考验。
event-stream 事件是开源供应链攻击的标志性案例,发生于 2018 年。攻击者 right9ctrl 以热心贡献者的身份取得了原维护者 dominictarr 的信任,获得了包的发布权限。随后,攻击者引入了一个名为 flatmap-stream 的恶意依赖,其中包含针对 Copay 比特币钱包的定向窃取代码。这起事件首次引起业界对"维护者社会工程"攻击向量的广泛关注,也暴露了开源社区中大量关键基础设施由单个志愿者维护的结构性脆弱。
此后,npm 生态陆续经历了 ua-parser-js(周下载量 700 万+,维护者账户被盗)、colors 和 faker(维护者主动投毒抗议)、node-ipc(维护者植入针对俄罗斯 IP 的破坏性代码)等事件。每一起事件都从不同角度揭示了信任链的脆弱:账户安全、维护者心理健康、政治动机等都可能成为攻击入口。
为应对这一系统性风险,行业正在推进多层防御框架。**SLSA(Supply-chain Levels for Software Artifacts,读作"salsa")**是由 Google 发起、OpenSSF(开源安全基金会)推动的供应链安全框架,定义了从 L1 到 L4 四个安全级别,涵盖构建过程的自动化、可审计性、隔离性和来源可验证性。具体而言,SLSA L1 要求构建过程有文档记录且自动化;L2 要求使用托管的构建服务并生成经过身份验证的来源证明(provenance);L3 要求构建环境完全隔离,防止维护者本人篡改构建过程;L4(目前仅为理论目标)要求所有依赖都经过双人审查且构建过程具有密封性(hermetic)。npm provenance 的实现对应了 SLSA L2-L3 的要求——它证明包确实由声明的 CI 系统构建,但不保证源代码本身未被篡改。这意味着如果攻击者获得了仓库的写权限(而非仅获取发布令牌),provenance 机制仍可能被绕过。
此外,OpenSSF Scorecard 项目为开源项目提供自动化的安全健康度评分,评估项目是否启用了分支保护、代码审查、依赖更新等安全实践。
开源软件的核心价值在于协作与共享,但这种开放性也天然地成为了攻击者的突破口。单个维护者账户的失守,就能通过密集的依赖网络波及成千上万的项目。这提醒整个行业:仅靠个人维护者的安全意识远远不够,平台层面的强制安全机制、自动化的异常检测以及社区的快速响应能力,才是遏制此类蠕虫式攻击的关键。
npm 平台已在近年逐步加强安全措施:2022 年起强制要求高影响力包(周下载量超过 100 万或拥有超过 500 个依赖者)的维护者启用 2FA;引入登录验证和可疑发布行为告警;支持细粒度访问令牌以限制令牌的包范围和 IP 范围。但这些措施仍然无法完全防御针对 CI/CD 环境的令牌窃取,因为自动化发布流程天然需要无交互的认证方式。
对于每一位开发者而言,将 npm install 视为一次需要审慎对待的信任行为,或许是这个时代最基本的安全素养。每一次依赖安装,本质上都是将代码执行权限授予了一个你可能从未审查过其源码的陌生人——理解这一点,是建立供应链安全意识的第一步。
相关推荐

机器学习研究入门:必读论文清单与研究实习申请路径
为ML初学者整理从零到研究实习的完整路径,包括必读经典论文清单(AlexNet、ResNet、Transformer等)、论文阅读方法、复现技巧及研究实习申请的实用建议。

Claude Code 入门实战教程:安装配置到自动化开发完整指南
详解Claude Code从环境搭建、权限配置、Go目标自主循环、Skills技能系统、MCP协议集成到版本控制的完整开发流程,帮助开发者快速掌握AI编程自动化工具。

Gemini 3.7 Flash发布与GPT-5.6极速模式:AI开源迈向生态时代
谷歌发布Gemini 3.7 Flash专注编程与Agent优化,OpenAI推出GPT-5.6 Ultra-Fast模式实现14倍速度提升。AI开源从开放模型转向开放生态,Agent工具链与成本监控工具密集涌现,智能体工作流进入实用化阶段。