npm爆发大规模供应链攻击 蠕虫污染 2000+ 个包版本: 深度技术剖析

npm 蠕虫污染 2000+ 个包版本:深度技术剖析

2026 年 8 月 4 日究竟发生了什么,以及每位开发者必须知道的事


全景概览

2026 年 8 月 4 日,攻击者入侵了 Jared Wray 的 GitHub 账号, 他是广泛使用的 keyv 包的维护者,该包是一个键值存储抽象库,每周下载量约 1.27 亿次。短短数小时内,一种名为 ChainDrop 的自我复制蠕虫(Shai-Hulud 恶意软件家族的变种)就污染了 444 个不同包中的 2,234 个版本 。这些受影响包的合计月下载量超过 20 亿次

这次事件之所以令人震惊,不仅在于其规模,更在于其精巧程度。该蠕虫并未利用 npm 本身的任何漏洞,而是滥用了合法功能:可信维护者的身份、自动化发布流水线以及经过签名的来源证明(provenance)。


攻击步骤详解

步骤 1:账户入侵

攻击者首先控制了维护者的 GitHub 账户。初始入侵途径尚未确认,但结果很明确:攻击者现在拥有了推送代码和发布包的合法凭证。

为何发生: 维护者的账户缺乏足够保护, 没有硬件安全密钥,未对关键操作强制要求双因素认证,或者凭证通过其他途径暴露。这是整个链条中最关键的一个失败点。


步骤 2:直接注入主分支

UTC 时间 8 月 4 日 09:02,攻击者直接向 keyv 仓库的 main 分支推送了一个恶意提交。该提交添加了两个文件:

  • setup.mjs ------ 一个 29 KB 的加载器脚本
  • Math_Symbol.js ------ 一个 728 KB 的混淆第二阶段载荷

同时修改了 package.json,添加了 preinstall 钩子:

json 复制代码
"scripts": {
  "preinstall": "node setup.mjs"
}

为何发生: 攻击者拥有主分支的写入权限。仓库没有配置分支保护规则,要求合并到主分支前必须经过 Pull Request 审查。这是 GitHub 仓库安全配置的基本失误。

攻击者得到了什么: 通过直接注入主分支,恶意代码成为了自动化发布流水线将会构建和发布的源码的一部分。


步骤 3:IDE 和 AI 代理持久化钩子

UTC 09:04,攻击者推送了额外的提交,在开发者工具配置中植入了执行钩子:

  • .claude/settings.json ------ 一个 SessionStart 钩子,在 Claude Code 会话启动时运行 node .claude/setup.mjs
  • .vscode/tasks.json ------ 一个 folderOpen 任务,在仓库于 VS Code 中打开时运行 node .vscode/setup.mjs

为何发生: 攻击者深知开发者不只是安装包,他们还会克隆仓库并在 IDE 中打开。这就创建了一个完全不需要 npm install 的次级感染向量。开发者或 AI 编码助手仅仅因为打开克隆的仓库文件夹就可能被攻陷。


步骤 4:发布到 npm

UTC 09:35,keyv@6.0.0 被发布到 npm。合法的发布流水线构建了已被木马化的源码,并附带了有效的 npm 来源证明 ------ 一份签名的 SLSA 证明。

复制代码
keyv@6.0.0 附带有效的 npm 来源证明发布
合法的发布流水线构建了已被木马化的源码
签名验证在恶意软件上通过了

为何发生: 来源证明证明的是构建完整性,而非源码完整性。构建系统执行了它设计要做的事, 构建仓库中存在的任何源码,并对结果签名。系统信任源码,因为维护者的账户是受信的。

关键教训: 经过签名的来源证明并不意味着代码安全。它只说明该代码确实来自所声称的构建过程。


步骤 5:蠕虫激活

当开发者运行 npm install 安装受影响包时,preinstall 钩子自动执行。setup.mjs 实际执行的操作:

  1. 检查 Bun 运行时 ------ 若未安装,则直接从 GitHub 官方发布页下载 bun v1.3.13
  2. 执行载荷 ------ 使用 Bun 运行 Math_Symbol.js
  3. 清除痕迹 ------ 执行后删除临时 Bun 目录

Math_Symbol.js 载荷经过重度混淆(728 KB),采用控制流扁平化和 Base91 字符串编码。

载荷功能:

  • 使用 TruffleHog 风格的正则表达式扫描磁盘,查找凭证信息
  • 窃取 npm 和 GitHub 访问令牌
  • 窃取 AWS、GCP 和 Azure 云凭证
  • 提取 HashiCorp Vault 令牌和 Kubernetes 服务账户令牌
  • 读取 GitHub Actions OIDC 令牌和实例元数据
  • 捕获 Stripe 和 Slack 令牌、SSH 密钥以及数据库凭证
  • 使用 AES-256-GCM 在操作者公钥下加密窃取的数据
  • 将数据外泄至攻击者控制的 GitHub 仓库和通过 DNS 解析的目标地址

步骤 6:自我复制

这才是它成为蠕虫的关键。利用窃取到的 npm 令牌,恶意软件:

  1. 搜索被入侵身份拥有发布权限的其他包
  2. 下载这些包
  3. 注入相同的恶意文件(setup.mjs 以及 Math_Symbol.jsmath_init.js
  4. 修改 package.json 添加 preinstall 钩子
  5. 增加版本号
  6. 将木马化后的包重新发布到 npm

速度令人咋舌: 蠕虫在大约两小时内(09:40 至 11:44 UTC)重新发布了 435 个包中的 1,557 个恶意版本。每隔两到七分钟,就有一波新的被污染包发布。

当窃取的凭据中包含 GitHub App 令牌时,蠕虫进一步扩大影响范围 ------ 向每个可访问仓库的最多 50 个分支提交恶意钩子。

为何发生: npm 令牌授予发布权限。一旦被窃取,它们就成为蠕虫的主要传播机制。整个过程没有任何速率限制或异常检测来标记这种异常的发布模式。


步骤 7:动态 C2 基础设施

恶意软件放弃了在载荷中硬编码 C2 域名的传统做法,而是:

  • 使用 eth_call 调用主网上的一个以太坊智能合约(StringListStore
  • 动态获取当前的 C2 域名列表
  • 攻击者可以随时更新合约来更换 C2 基础设施,而无需重新部署恶意软件

为何发生: 静态 C2 域名容易被封堵。通过将以太坊作为动态配置存储,攻击者使恶意软件能够抵御域名封禁。初始链记录显示有 3 个域名,后来更新为仅返回一个。


步骤 8:令牌撤销陷阱

这里尤其阴险。载荷安装了一个自毁开关

  • 将窃取的 GitHub 令牌和攻击者提供的处理命令写入 ~/.config/gh-token-monitor/
  • 将自身持久化为 macOS LaunchAgent 或 Linux systemd 用户服务,并启用 loginctl enable-linger 以在注销后继续运行
  • 监控令牌是否被撤销

陷阱: 如果你作为第一响应措施撤销了被窃令牌,就会触发自毁开关。载荷将执行其处理命令 ------ 可能销毁证据、外泄额外数据或造成其他破坏。

为何发生: 攻击者预判了应急响应流程,并内置了对抗措施。这是专业级别的战术手段。


受影响的包

值得注意的被入侵版本包括:

  • keyv@6.0.0 ------ 月下载量约 6 亿+
  • flat-cache@6.1.24 ------ 月下载量约 5.8 亿
  • file-entry-cache@11.1.6 ------ 月下载量约 5.7 亿
  • cacheable-request@13.0.20
  • cacheable@2.5.1
  • cache-manager@7.2.10
  • ecto@5.0.1

蠕虫还波及了 Deliveroo、Ornikar、OneReach、Picsart、Qlik 和 ServiceTitan 等其他组织下的包。


代码分析:恶意载荷

preinstall 钩子

json 复制代码
{
  "scripts": {
    "preinstall": "node setup.mjs"
  }
}

问题所在: npm 的 preinstall 钩子在包安装之前 执行,且无需任何用户交互。受害者不需要导入包或编写任何应用代码 ------ 仅仅运行 npm install 就足够了。

攻击面: 这并不是 npm 的漏洞,而是合法功能被滥用。包管理器信任包作者不会作恶。


加载器(setup.mjs ------ 简化示意)

javascript 复制代码
// 伪代码示意
import { platform, arch } from 'os';
import { execSync } from 'child_process';
import { existsSync, mkdtempSync, rmSync } from 'fs';

// 检查 Bun 是否已安装
const bunPath = whichBun();
if (!bunPath) {
  // 从 GitHub 下载 Bun v1.3.13
  const url = `https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-${platform()}-${arch()}.zip`;
  downloadAndExtract(url);
}

// 执行混淆后的载荷
execSync(`bun run ${__dirname}/Math_Symbol.js`, { stdio: 'inherit' });

// 清理
rmSync(tempDir, { recursive: true, force: true });

危险之处: 加载器下载了一个完整的 JavaScript 运行时(Bun)并执行任意代码。这绕过了任何 Node.js 特定的安全控制。728 KB 的载荷经过重度混淆,使得静态分析极为困难。


凭证收割器(Math_Symbol.js ------ 概念性)

javascript 复制代码
// 基于模式的凭证扫描(TruffleHog 风格)
const patterns = {
  aws: /AKIA[0-9A-Z]{16}/g,
  github: /gh[psu]_[0-9a-zA-Z]{36,}/g,
  npm: /npm_[a-zA-Z0-9]{36,}/g,
  // ... 数百种其他模式
};

// 扫描磁盘上的文件
function scanDirectory(path) {
  for (const file of walkSync(path)) {
    const content = readFile(file);
    for (const [type, pattern] of Object.entries(patterns)) {
      const matches = content.match(pattern);
      if (matches) {
        exfiltrate({ type, matches, file });
      }
    }
  }
}

// 自我复制
function propagate(npmToken) {
  const packages = getPublishablePackages(npmToken);
  for (const pkg of packages) {
    const tarball = downloadPackage(pkg);
    const modified = injectPayload(tarball);
    const newVersion = bumpVersion(pkg.version);
    publishPackage(pkg.name, newVersion, modified, npmToken);
  }
}

之所以如此有效的原因:

  • 使用合法的 npm API 进行传播 ------ 无需任何漏洞利用
  • 为修改后的包重新计算完整性哈希值
  • 为篡改后的版本生成全新的 Sigstore 来源记录
  • 安装后包的行为正常,但主机已被攻陷

这对开发者意味着什么

攻击面已经扩大

此次攻击表明,供应链威胁模型现在包括:

  1. 维护者账户 ------ 开源中最关键的资产
  2. 源码仓库 ------ 不仅是已发布的包
  3. IDE 配置 ------ .vscode/.claude/.cursor/ 现在成为执行入口
  4. AI 编码助手 ------ 打开仓库即可触发执行
  5. 构建流水线 ------ 带有 OIDC 令牌的 GitHub Actions 是重点目标

来源证明并非银弹

keyv@6.0.0 版本具有有效的 npm 来源证明,附带签名的 SLSA 证明。证明验证通过,因为:

  • 构建系统从被入侵的源码构建
  • 构建系统对结果进行了签名
  • 签名证明该构建确实来自合法的流水线

来源证明证明的是构建完整性,而非源码完整性。这是业界许多人一直忽略的关键区别。


如何修复:补救指南

立即行动

1. 检查是否受影响

查找以下入侵指标(SHA-256 哈希值):

复制代码
9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc
  → Math_Symbol.js / math_init.js(728 KB 载荷)

54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668
  → setup.mjs(加载器,29,918 字节)

fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb
  → setup.mjs(加载器变体,11,017 字节)

检查你的 package-lock.jsonyarn.lock 中是否存在受影响的版本。

2. 不要首先撤销令牌

如果你怀疑被入侵,切勿作为第一步就撤销 npm 令牌或 GitHub 令牌 ------ 这会触发自毁开关。正确做法:

  • 首先识别并终止持久化机制
  • 删除 systemd 服务或 LaunchAgent
  • 删除 ~/.config/gh-token-monitor/
  • 然后轮换凭证

3. 识别并移除受影响版本

bash 复制代码
# 检查锁文件中是否存在受影响版本
grep -E '"keyv": "6\.0\.0"' package-lock.json
grep -E '"flat-cache": "6\.1\.24"' package-lock.json
grep -E '"file-entry-cache": "11\.1\.6"' package-lock.json

移除受影响版本,改用已知良好版本。

4. 将受影响系统视为已被入侵

任何安装了受影响包或打开了受感染仓库的系统,都应被视为可能已被入侵。从干净镜像重建这些系统。

5. 轮换所有暴露的凭证

在移除持久化机制后,轮换:

  • npm 令牌
  • GitHub 令牌和 PAT
  • 云服务商凭证(AWS、GCP、Azure)
  • HashiCorp Vault 令牌
  • Kubernetes 服务账户令牌
  • SSH 密钥
  • 数据库凭证
  • Stripe 和 Slack 令牌

长期预防

对于维护者:

  • 启用分支保护 ------ 要求合并到主分支前必须经过 Pull Request 审查,禁止直接推送。
  • 使用硬件安全密钥 ------ 对所有关键操作强制使用 WebAuthn 双因素认证。
  • 审计发布权限 ------ 定期审查谁和什么可以发布包。
  • 监控异常活动 ------ 留意意外提交,尤其是对主分支的提交。
  • 使用独立账户 ------ 维护者账户用于编写代码,使用单独且权限受限的服务账户进行自动化发布。

对于组织:

  • 在 CI 中阻止安装脚本 ------ 在 CI/CD 流水线中尽可能使用 npm install --ignore-scripts
  • 扫描依赖 ------ 使用能够检测恶意 preinstall 钩子和混淆代码的工具。
  • 锁定依赖版本 ------ 使用精确版本号,而非 ^~ 范围,以避免意外的补丁升级。
  • 审计锁文件 ------ 定期审查 package-lock.json 是否有意外更改。
  • 将仓库视为执行面 ------ 在隔离环境中克隆和打开仓库,尤其是在调查安全事件时。

对于生态:

  • npm 应考虑对多年未使用 preinstall 钩子却突然添加该钩子的包进行额外验证。
  • GitHub 应考虑标记那些通常使用 PR 流程却突然直接向主分支提交的账户。
  • Sigstore 来源证明应包含源码完整性验证,而不仅仅是构建完整性。

真正的问题:没有验证的信任

此次事件从根本上说并非 npm、GitHub 或任何特定技术的漏洞,而是关于 我们如何将整个软件供应链建立在隐式信任之上

我们信任了:

  • 维护者账户是安全的
  • 仓库中的源码是维护者所意图的
  • 来源签名意味着代码是安全的
  • 包管理器在安装期间只执行安全代码

这些假设没有一项成立。

攻击链之所以优雅,并非因为它利用了一个零日漏洞,而是因为它利用了 信任 ------ 我们对维护者、构建系统、包注册表以及日常工具的信任。


结语

2026 年 8 月的 npm 蠕虫攻击是软件供应链安全的一个分水岭。它表明:

  • 账户入侵已成为新的零日漏洞 ------ 最关键的漏洞不在代码中,而在身份中
  • 来源证明不等于安全 ------ 一个已签名的包仍然可能是恶意的
  • 你的 IDE 现在是攻击向量 ------ 打开仓库与运行未知代码同样危险
  • AI 编码助手是攻击目标 ------ 它们会在无人审查的情况下执行配置钩子
  • 应急响应必须进化 ------ 旧的行动手册(先撤销,后提问)可能会适得其反

维护者是受害者。npm 和 Sigstore 流水线完全按照设计运行。系统按设计工作 ------ 而这正是问题所在。

我们需要构建不依赖隐式信任的系统。每一步都需要验证。我们必须认识到,在开源世界中,维护者的笔记本电脑现在已成为攻击面的一部分

相关推荐
huabuyu1 小时前
治好 AI 长回答的卡、闪、膨胀:渲染开销从 50 万次砍到 1000
前端·javascript
嘟嘟07171 小时前
学了 useContext 还是不理解?从 prop drilling 到 useMouse 的一次完整复盘
前端·javascript·react.js
玉宇夕落1 小时前
React useContext 与自定义 Hook —— 从零到一理解跨组件通信与状态共享
前端
涛涛ing1 小时前
一行命令让AI帮你修性能问题:perfpatch正在改变前端优化的游戏规则
前端
huabuyu1 小时前
模型吐到一半的 JSON 为什么不崩、不卡、不抖?流式 Function Call 的三层修复
前端·javascript
触底反弹1 小时前
JS 运行原理与 Event Loop:从 Web Worker 实战说起
前端·javascript·react.js
无糖可可果1 小时前
React Context 与自定义 Hooks 学习分享
前端
胡萝卜术1 小时前
声明式与命令式的边界:从鉴权路由守卫到 useRef 的引用哲学
前端·javascript·面试
Python私教1 小时前
如意 Django CRM 容器化实战:后端、前端、数据库、Redis 的协同启动逻辑
前端·数据库·django