摘要:ChainDrop 借助被盗维护者身份、安装脚本和 CI 发布权限形成自传播闭环,部分恶意版本甚至携带有效 provenance。本文拆解攻击链、证据边界、应急处置与长期治理。
2026 年 8 月 4 日,npm 生态出现了一轮速度异常的恶意发布:keyv、flat-cache、cache-manager 等原本由不同发布者维护的包,短时间内连续出现可疑补丁版本。它们保留正常功能,只在包顶层多出加载器与高度混淆的 JavaScript,并在 package.json 中增加 preinstall。安装者看到的可能只是一次无报错的依赖更新,后台却已经启动凭据搜集、外传和下一轮发布。
多家安全团队将这轮活动称为 ChainDrop,并把其载荷归为 Mini Shai-Hulud 的变体。这里首先要把名字和数字说清楚:ChainDrop 是研究机构为本轮行动及其蠕虫载荷使用的追踪名,并非 npm 官方漏洞编号。StepSecurity 在事发当天统计到 444 个包、2,212 个恶意版本,在不足四小时内发布;Microsoft、Elastic 与 Unit 42 的公开分析均采用“超过 400 个包”的保守表述。部分媒体随后报道“超过 1,300 个包、约 20 亿月下载量”,但截至本文写作时,这个包数量没有得到上述几家一手技术报告的共同确认,也可能混入后续样本、版本数或不同统计口径。因此,本文以“至少 400 余个包、2,000 余个恶意版本”为可交叉核验的规模,不把下载量直接等同于实际感染设备数。
这一区分很重要。包的历史下载量体现潜在暴露面,却无法回答有多少构建在恶意版本存活期间实际解析、下载并执行了安装脚本;锁文件、私有镜像、缓存、版本范围和发布时间都会改变实际命中率。ChainDrop 已足够严重,不需要靠放大数字来证明。
起点是一条被接管的发布身份
公开证据指向 keyv 维护者相关身份首先失守。Elastic 称攻击者接管了该维护者并植入多个子包;Microsoft 认为初始访问与被盗维护者凭据一致,随后传播则明确使用窃取到的 npm 发布令牌,并在特定场景中尝试 GitHub Actions OIDC 发布权限。需要保留的一点是:公开报告尚未完整披露最初凭据究竟通过钓鱼、会话窃取、恶意依赖还是其他路径取得。能确认的是结果,不宜把尚未证实的入侵手法写成事实。
事件暴露了“源码仓库身份”和“注册表发布身份”之间的断层。Microsoft 观察到,不少恶意补丁版本没有对应的源代码提交、拉取请求、标签或正常发布记录。这说明攻击者并不需要逐个攻破公开仓库:拿到具备写权限且可绕过双因素验证的 npm token 后,就能下载注册表中的最新 tarball,在本地注入文件、提升补丁版本,再直接发布。代码审查仍然干净,仓库默认分支仍然干净,npm 上被用户实际安装的归档却已经变了。
这也是账号接管在包生态里比普通 SaaS 账号失窃更危险的原因。一个维护者身份往往拥有多个包,组织账号又可能跨越多个产品;自动化 token 常驻 CI,权限范围多年未收紧。攻击者接管的是一组可以向海量下游交付可执行代码的发布权,影响远超单个登录页面。
preinstall 把“下载依赖”变成了“执行依赖”
ChainDrop 的第一段可见改动很小:package.json 的 preinstall 指向 setup.mjs。npm 的生命周期脚本本来用于本机编译、原生模块准备等正当工作,但旧版 npm 默认会在依赖安装期间执行它。于是,恶意代码不必等待应用启动,也不必被业务代码 import;只要依赖解析命中恶意版本,开发机或构建机就可能在安装尚未结束时执行载荷。
加载器检查环境后,按平台下载官方 Bun 运行时,再用 Bun 执行约七百 KB、经过控制流平坦化和字符串编码的第二阶段文件 Math_Symbol.js 或 math_init.js。Bun 本身没有被攻破,它只是被当作便携执行器。载荷覆盖 Linux、macOS 和 Windows;在普通开发机上,它会脱离当前进程继续运行,在 CI 中则留在当前任务内,以便读取作业上下文中的秘密和短期身份。部分样本还会在俄语区域设置的主机上退出。
这种设计绕过了许多团队习惯性的检查时点。单元测试、应用启动时的运行时防护,甚至“安装完成后再扫描工作目录”的流程,都可能来得太晚。执行边界已经前移至包管理器解包和生命周期脚本阶段。
npm 12 在这方面改变了默认值:依赖安装脚本默认阻止,只有列入 allowScripts 的包才获准运行。它能直接压缩 ChainDrop 这类 preinstall 路径,但覆盖范围有限。项目仍可能主动批准危险脚本,旧版客户端和其他包管理器仍有不同语义,攻击者也可能转向构建插件、测试配置、编辑器任务或正常运行路径。正确做法是把“哪个依赖为何需要安装脚本”变成可审计的白名单;升级客户端只能作为控制措施之一,不能据此结案。
它搜集整个工程身份平面的凭据
第二阶段捕获环境变量、配置文件、shell 历史、SSH 私钥,并用所得凭据调用服务 API 验证权限、继续枚举。公开分析列出的目标包括 npm、GitHub、AWS、GCP、Azure、Kubernetes、Vault、Docker、Terraform、PyPI,以及多种 AI 开发工具。Unit 42 还逆向出读取 Linux GitHub Actions Runner 进程内存的代码,试图从 Runner.Worker 捕获 OIDC token 和 runner secret。
因此,CI runner 命中恶意版本后,不能只轮换 npm token。该作业可见的云身份、仓库写权限、制品库凭据、部署密钥、Vault 秘密和 OIDC 可兑换权限,都在暴露面内;短期凭据也可能在失效前被兑换为新能力。
窃取数据经压缩和混合加密后外传。样本通过以太坊合约查询主要 C2,使操作者无需更新载荷即可换域名;失败时还可借受害者 GitHub 账号创建公开仓库外传。Unit 42 发现 C2 响应具备返回并执行附加 JavaScript 的能力,但测试只收到空响应:远程下发代码是已实现能力,是否已被普遍使用则证据不足。
载荷还会向仓库写入 .vscode/tasks.json、.claude/settings.json 与配套加载器,让打开 VS Code 文件夹或启动 Claude Code 会话成为再次执行的触发点。研究人员对其中部分交叉引用的实际可达性有细节差异,但防守上不应纠结于单一路径是否完美:编辑器与 AI 编码工具的项目级配置已经进入供应链攻击面,必须像 CI workflow 一样接受审查。
自传播闭环:凭据既是战利品,也是燃料
ChainDrop 与普通信息窃取器的分水岭,是它把盗来的 npm 身份立即接回发布流程。对满足“可写包且可绕过 2FA”的 token,载荷会:枚举该身份能发布的所有包;下载每个包最新 tarball;注入加载器和混淆载荷;加入 preinstall;提升 patch 版本;重新打包并发布。被修改的包仍保留原有功能,因此冒烟测试和业务回归可能全部通过。
传播由此形成闭环:包 A 的安装脚本在维护者或 CI 上运行,搜出该环境的 npm token;token 允许发布包 B、C、D;这些包的新版本又进入更多开发机和流水线,继续搜索新的发布身份。攻击者不需要人工挑选每个目标,传播速度取决于权限图、自动更新速度与注册表响应,而不再取决于人工操作速度。四小时内出现数千个版本,正是这种身份驱动复制的结果。
它还尝试把 GitHub 写权限变成第二条传播路径:在多个分支植入编辑器或 AI 工具配置,或通过工作流获取秘密。此时“依赖树”已不足以描述风险,完整的传播图同时连接 npm 包所有权、GitHub 仓库权限、CI 身份、开发工具钩子和云 IAM。只在 SBOM 中搜索恶意包名,会漏掉已经由被盗身份发布的新版本、仓库持久化以及后续制品污染。
有效 provenance 为什么仍可能为恶意制品背书
ChainDrop 最值得警惕的技术细节在于,它可能无需伪造签名。
Microsoft 发现载荷包含针对 npm trusted publishing 的 GitHub Actions OIDC 路径:恶意代码若运行在合法发布 workflow 内,可以请求 npm 受信发布凭据。Unit 42 对样本的逆向显示,一条专门针对 opensearch-js 发布工作流的路径还会请求 Sigstore OIDC 身份,生成 in-toto SLSA v1 provenance、用临时密钥签名并写入 Rekor 透明日志。Unit 42 明确表示没有观察到该专用路径实际执行,而且它受特定仓库与 workflow 条件约束;因此不能据此断言相关项目已经发布了带有效证明的恶意版本。但这段可达实现足以说明边界。
SLSA 对 provenance 的定义,是关于制品“在哪里、何时、如何产生”的可验证信息;构建 provenance 将输出追溯到所用源码和构建过程。它解决的是来源可追溯与构建身份真实性,不等价于代码无恶意、依赖无投毒、workflow 未被攻击者控制。如果合法仓库的合法 workflow 在合法 runner 上执行了攻击者代码,那么“该制品确由这个 workflow 产生”可以完全为真,签名验证也可以完全成功。
因此,provenance 检查不能停在一个布尔值。消费者至少要验证:证书中的仓库、workflow、ref 和构建者身份是否符合预期;发布是否对应受保护标签和经过审查的提交;构建输入与生成 tarball 是否可复现或可比对;透明日志中的发布时间和频率是否异常;workflow 文件与第三方 Action 是否被固定到可信摘要。维护方则要把受信发布与受保护环境、最小 id-token 权限、隔离且短命的 runner、发布审批绑定起来。
换句话说,trusted publishing 消除了长期 npm token,是重要进步;但它把信任集中到了 CI 身份和 workflow 完整性。SLSA provenance 提供证据,却不会替组织判断“这个构建过程是否值得信任”。ChainDrop 展示了把来源证明误当成内容安全判决所产生的盲区;这并不说明 SLSA 失效。
传播机制的关键变量:权限图、时间窗与自动化程度
仅按依赖层级计算影响,会低估这类蠕虫的扩散速度。传统依赖投毒通常沿“上游包—下游应用”方向传播,ChainDrop 同时沿身份权限横向复制。某个包的安装量很低,只要其维护环境里保存着可发布多个高流量包的 token,它就可能成为高价值跳板;某个热门包即使下载量很高,若恶意版本很快撤下、下游严格锁定版本且禁用脚本,实际感染又可能远低于历史下载量暗示的规模。
组织可以把传播风险拆成四个变量。第一是命中概率:版本范围是否接受新 patch,锁文件是否更新,镜像是否自动同步,缓存是否已拉取恶意 tarball。第二是执行概率:客户端是否运行依赖生命周期脚本,安装发生在开发机、CI 还是仅做静态镜像。第三是凭据产出:被感染环境能看到哪些 token、SSH key、云身份和 runner 内存,权限是否允许写入或再次发布。第四是复制速度:自动化发布能否无审批完成,注册表新版本多久进入下游,安全团队多久发现并撤下样本。四项相乘,才接近一次安装带来的后续扩散能力。
这一模型也解释了为何短暂暴露仍需严肃处置。恶意版本即使只在线几十分钟,也可能被公共或企业镜像永久缓存;一次 CI 安装便可产生新的发布身份,后续恶意版本来自另一名合法维护者,初始包撤下后传播仍会继续。排查时间窗因此不能只看 npm 的下架时刻,还要覆盖镜像同步、缓存保留、构建排队和制品发布时间。团队应把“解析过”“下载过”“执行过”“泄露过”“使用泄露身份发布过”分成五种状态,逐级收集证据,避免把下载记录直接当感染,也避免因没有运行告警便草率排除风险。
provenance 的信任边界需要逐层核验
provenance 至少涉及四个彼此独立的判断。第一层是身份真实性:签名证书和透明日志能否证明某个仓库、workflow 或构建者参与了发布。第二层是调用授权:该 workflow 是否由受保护 ref、经过批准的环境和预期事件触发。第三层是输入完整性:源码提交、依赖、构建脚本、第三方 Action、基础镜像与下载内容是否符合策略。第四层是输出安全性:生成物是否含恶意逻辑、越界能力或未经审查的差异。前一层通过,无法自动替后一层作结论。
ChainDrop 针对 trusted publishing 的尝试落在第二层与第三层之间:攻击者可以借合法 runner 和合法 OIDC 联邦获得短期发布身份,签名链条仍然有效,但触发上下文或构建输入已经受污染。此时校验证书主体只能说明“谁签了”,无法回答“为何触发”“输入是否经过审查”“归档与源码是否一致”。消费者若只执行 verify=true,就把复杂的信任判断压成了容易误读的一位布尔值。
更稳妥的策略应绑定一组约束:仅接受指定仓库和固定 workflow 路径;要求 tag 指向受保护分支中经过评审的提交;核对构建者 ID、事件类型和环境;第三方 Action 固定完整 commit 摘要;构建期间禁止任意网络下载;发布 tarball 与干净环境重建结果做文件级或语义级比对;对无源码变更却出现的新版本、短时连续 patch、非常规时段发布设置额外审批。若项目暂时无法可复现构建,也应保存打包清单、依赖解析结果、workflow 摘要和制品哈希,让事后调查能重建证据链。
信任边界还包含签名服务之外的组件。GitHub 账号恢复流程、分支保护管理员、云端自托管 runner、组织级 reusable workflow、npm 包所有者列表和注册表支持人员,都可能改变发布权限。长期治理需要定期枚举这些主体,并记录谁可以修改发布 workflow、谁可以批准环境、谁可以新增包 owner、谁可以调整 OIDC 条件。provenance 能提高透明度,前提是组织没有把所有关键变更都交给同一个可被接管的身份。
组织排查要建立证据矩阵
面对数百个包和数千个版本,简单群发“请检查是否安装过”很快会失控。较有效的做法是由事件指挥统一时间窗和判定标准,把资产按开发终端、共享 runner、自托管 runner、构建镜像、私有镜像、制品库、发布账号和云租户分组;每组指定负责人,提交结构化证据。最低字段包括主机或作业 ID、安装时间、包版本、tarball 哈希、脚本策略、进程与网络证据、可见凭据、后续发布和处置状态。
证据矩阵可分四级。暴露表示依赖解析或镜像同步命中可疑版本;执行表示安装脚本、Bun 或第二阶段载荷有运行迹象;身份泄露待定表示环境存在载荷可读取的凭据,即使暂未看到外传;确认扩散表示凭据被调用、仓库被修改、包被发布或下游制品受污染。每一级对应不同动作:单纯暴露需要清缓存和锁版本;确认执行需要重建主机并扩大日志保全;凭据泄露待定需要撤销和审计;确认扩散则要通知下游、撤回制品并追踪二级受害者。
排查中最容易出现两个误区。一是只搜文件名。混淆载荷、文件名和 C2 都可以变化,行为证据更耐用,例如 npm 安装进程下载 Bun、runner 在构建期间调用 npm owner API、短时批量 npm publish、无提交对应的新版本、GitHub 账号创建异常公开仓库。二是只查默认分支和当前容器。ChainDrop 涉及多分支写入、编辑器配置、缓存层和 runner 内存,调查必须覆盖历史 workflow、所有可写分支、镜像层、作业日志和注册表审计记录。
跨团队协作也要避免“先轮换再说”导致证据消失。安全团队负责隔离、取证范围和 IOC;平台团队冻结发布、导出 runner 与 OIDC 日志;开发团队确认依赖用途和正常发布基线;身份团队撤销令牌并检查异常兑换;法务与沟通团队判断通知义务。轮换顺序应按攻击路径安排:先停止发布和仓库写入能力,再撤销可用于持久化或横向移动的云与代码托管身份,随后恢复低权限凭据。所有新凭据只能在干净环境生成。
长期治理要削减可复制的权限边
应对蠕虫的长期目标,是让一次代码执行难以兑换成下一次公开发布。发布账号应逐步移除长期 token,采用短时 OIDC 身份,并用仓库、workflow、ref、环境和事件类型约束联邦条件;包所有权按需拆分,避免一个维护者默认拥有大量无关包;高影响包的新增 owner、权限提升和紧急发布采用双人审批。CI 作业使用一次性 runner,按项目和信任等级隔离,fork PR、依赖安装、测试与发布处于不同权限域。
依赖治理也应体现时间因素。企业镜像可对新版本设置观察期,在同步前检查源码标签、provenance、维护者变更、安装脚本和恶意情报;紧急升级通过显式豁免完成并保留审批记录。构建默认使用锁文件和不可变哈希,生产流水线禁止自动接受新 patch。需要安装脚本的包维护清单、责任人和理由,定期复核;脚本在无秘密、无发布权限、无公共出口的阶段运行,生成物经扫描后再传入后续作业。
检测重点应覆盖身份与发布行为。可建立每个包的正常基线:常见发布时段、发布者、workflow、版本节奏、tarball 文件清单、脚本字段和 provenance 主体。出现维护者短时发布多个无关包、源码未变却发布 patch、归档突然新增执行器、runner 调用链上 RPC、包安装期间访问 npm 发布 API 等组合信号时,自动暂停镜像同步或发布。单个弱信号可能来自正常维护,多个信号同时出现则应升级为事件。
最后,组织要定期演练“合法身份发布恶意制品”的场景。演练应验证能否在小时级列出命中版本的构建,能否撤销所有发布身份,能否从干净基线重建制品,能否联系内部应用与外部客户,并能否解释每份制品对应的源码、workflow 和审批。只有把这些能力做成日常平台功能,provenance、短期凭据和安装脚本策略才会形成闭环,避免在下一次事件中重新依赖人工拼表。
应急处置必须按“代码执行事件”展开
如果组织在 8 月 4 日前后解析过受影响版本,应先隔离开发终端和 runner,避免直接在可疑主机上登录各平台改密码;同时冻结发布,保留进程、网络、工作流日志、npm/yarn 缓存与制品元数据。凭据撤销和轮换要从已知干净的设备执行,避免新秘密再次被读取。
排查需要同时覆盖五个层面:
- 依赖与缓存:核对锁文件、依赖树、私有镜像、构建缓存和容器层中的确切包版本与哈希;清除恶意 tarball,固定已知良好版本,并设置新版本等待期。
- 端点与 runner:查找
setup.mjs、Math_Symbol.js、math_init.js,以及 Node 拉起 Bun、Bun 访问异常域名、临时目录执行后删除等行为;共享 runner、基础镜像和黄金镜像应重建而非原地“清理”。 - 身份:撤销 npm、GitHub PAT/会话、SSH、云、Kubernetes、Vault、制品库及 AI 工具凭据;审查 OIDC 联邦关系、角色信任策略和事件窗口内的令牌兑换。轮换顺序应先切断传播与发布能力,再恢复低权限访问。
- 仓库与发布:搜索无对应提交或标签的 npm 版本、异常 patch 连发、未知 provenance、可疑 GitHub workflow,以及
.claude、.vscode中新增的自动执行配置;检查所有可写分支,不只默认分支。 - 下游制品:任何在可疑 runner 上构建、签名或推送的 npm 包、容器、桌面应用和部署包都应视为完整性待定,从干净环境和可信依赖基线重新构建,再比较哈希与证明。
长期修复则要减少蠕虫可利用的边。升级 npm 12 并采用安装脚本默认拒绝;为确需脚本的依赖维护白名单;使用最小权限、短时效的受信发布,淘汰长期且可绕过 2FA 的自动化 token;发布 workflow 使用受保护环境和人工审批;CI runner 按作业销毁,阻止其读取无关项目秘密;为依赖升级设置 24 至 72 小时观察窗口;监控“源码无变更但注册表出现新版本”、单一身份短时发布多个无关包、构建期间访问链上 RPC 或陌生域名等异常。
最关键的观念变化是:软件供应链不只是代码从仓库流向制品的单向流水线,也是一张可被身份反向遍历的权限网络。ChainDrop 找到一个发布者后,会沿着 token、包所有权、CI OIDC、仓库写权限和开发工具配置继续移动。防线若只证明“包来自哪里”,却不限制“谁能让那里产出什么”,下一次带着有效签名的恶意发布仍会畅通无阻。
来源
- Microsoft Threat Intelligence, ChainDrop supply chain compromise: Anatomy of a self-propagating worm
- Elastic Security Labs, Shai-Hulud strikes again: CHAINDROP worm hits 400+ npm packages
- StepSecurity, ChainDrop npm Worm: Bun-loaded CI/CD credential harvester with Ethereum dead-drop C2
- Unit 42, ChainDrop: Inside a Self-Propagating npm Worm
- SLSA specification, Provenance
- npm Docs, npm-install-scripts(npm CLI v12)