Axios 202603 投毒事件杂谈:CI 最佳实践、供应链安全与选型反思
2026年4月4日Elecmonkey
Axios 投毒事件
UTC 时间 2026 年 3 月 31 日,axios npm 包遭遇投毒时间,攻击者通过劫持了有发包权限的维护者的账号悄悄发布了两个带有病毒的版本 axios@1.14.1 和 axios@0.30.4,这是 JS 生态一起极其典型的供应链攻击案例。从带毒依赖发布到被 npm 官方下架,整个事件的时间窗口大约是 3 小时左右。
攻击方式是而是“依赖投毒”,攻击者没有动 Axios 代码,而是添加了一个伪装成正常包的 npm 包作为依赖,然后再这个包的 postinstall 脚本里去执行恶意代码。axios 是一个下载量极大的包,根据 npm 数据,该恶意依赖周下载量为 276,209,目前 npm 官方已经下架并发布占位符 package 避免更大的影响。
考虑到 axios 在 JavaScript 生态中的地位,在窗口期内如果有人对依赖进行了升级,或者创建了新项目 / 安装了没有锁文件的项目的依赖,这次攻击极有可能盗取一些开发者本机的敏感信息。对于本纪
近期爆火的 openclaw 的依赖链条中也有 axios
bash1openclaw 2└─ @line/bot-sdk@10.6.0 3 └─ axios@1.13.6
CI 环境和生产环境请务必隔离
使用 pnpm
pnpm v10 默认不会自动执行依赖的 postinstall 脚本,官方还建议只对白名单依赖放行构建脚本,而不是全局重新打开。如果在事发时用的是 pnpm v10,且保持默认配置,没有把相关依赖加入允许执行脚本的白名单,也没有全局放开构建脚本,那么这次攻击里最关键的 postinstall 恶意执行环节很可能会被拦住。
pnpm 的其它好处不用再赘述了,明显更快的依赖解析速度,通过符号链接节省磁盘空间,等等等等。
隔离 CI 环境和生产环境
笔者曾经参与运维的某一个项目,每当代码有更新,生产环境的更新方法是在生产服务器 git pull 下来,然后重新 npm run build. 当然,这和环境里的某些基础设施的缺失和网络拓扑有关,但是确实这么做太抽象了。当时那个项目用的还是 npm,在这场攻击中风险是相当高的。
将 lockfile 提交到 Git 仓库
首先众所周知,并不是你 package.json 里写什么版本号安装下来就是什么版本号,SemVer 机制的存在让大部分项目都会声明在一个范围内安装依赖版本 —— ^ 开头的版本号表示只要大版本一致就 ok,~ 开头的版本号表示前两位一样。也就是说,如果你 package.json 里写了 ^1.14.0,依赖树解析时会安装到 1.*.* 的最新版本。其次,不是写死了版本号(不用 ^ 和 ~)就能锁死依赖版本的。如我前面举例子的 OpenClaw,你不只有自己的依赖,你的依赖还有它们的依赖,你的依赖的依赖还有它们的依赖……
这两点的意思就是 lockfile 为什么要存在 —— 它记录了一个确切的、可复现的依赖树,而不是 SemVer 机制下的一个模糊范围。不管是 npm 还是 pnpm(我猜 yarn 也一样),在 lockfile 存在的时候都会优先走 lockfile. 如果 lockfile 已经提交到 Git 仓库了,在 CI 环境里安装依赖的时候,除非特意去删除 lockfile,否则是不会被升级到带毒版本的。
一般在 CI 环境会去跑
bash1pnpm i --frozen-lockfile
来强行要求 pnpm 不许重新解析依赖树,提高安全性。
除了安全性,再加两条把 lockfile 提交到 Git 仓库的理由
- 提高 CI 速度,省去重新解析依赖树的时间
- 在不同的环境复现出相同的结果,避免离奇的 bug
当然,当然,我只想在仓库里看到一个 lockfile. 谁懂我在某个仓库里同时看见 package-lock.json、pnpm-lock.yaml 和 yarn.lock 时的心情…… (其实我的反应是我应该再提交一个 bun.lock 上去x
该不该在 package.json 锁死依赖版本
追求安全的第三方库或者框架可能会锁死依赖版本。我自己认为对于业务项目来说,在正确使用 lockfile 的情况下,锁死依赖版本的收益是有限的。当然,我也不认为就有什么明显的坏处。如果项目对安全性有特别的要求,或者对风险极端敏感,对安全更有意义的选项可能是在更新的时候引入对依赖的审查机制,同时尽可能减小第三方依赖的数量。
供应链安全与网络请求库选型反思
XMLHttpRequest(下简称 XHR) 最初作为 Internet Explorer 5 的一个私有 API 被引入,开起来一个名为 AJAX(Asynchronous JavaScript and XML)的新时代,随后 XHR 被 w3c 标准采纳,成为浏览器的标配功能。
但是从名字就能看出来 XHR API 设计的极端不优雅 —— 我们仍不知道微软这家公司当年对 .xml 究竟有多少执念。回调地狱也是其“不优雅”的一部分 —— 但这并不是独属于 XHR 的问题。但是由于其标准化足够早,浏览器兼容性好的出奇。另一个角度说,也是 XHR 本身功能强大且完备,因此我们其实只缺一个封装层,让我们的网络请求变得优雅。
体验一下哈。
javascript1function getUser() { 2 const xhr = new XMLHttpRequest(); 3 4 xhr.open("GET", "/api/user?id=123", true); 5 6 xhr.onreadystatechange = function () { 7 if (xhr.readyState === 4) { 8 if (xhr.status >= 200 && xhr.status < 300) { 9 const data = JSON.parse(xhr.responseText); 10 console.log("成功:", data); 11 } else { 12 console.error("请求失败:", xhr.status, xhr.statusText); 13 } 14 } 15 }; 16 17 xhr.onerror = function () { 18 console.error("网络错误"); 19 }; 20 21 xhr.send(); 22}
于是有了 Axios,作为一个轻量级的 HTTP 客户端库,Axios 提供了一个基于 Promise 的 API,支持浏览器和 Node.js 环境。它的设计目标是提供一个更简洁、更易用的接口,同时还提供了一些额外的功能,如请求和响应拦截器、自动转换 JSON 数据、取消请求等。
2015年,与 ECMAScript 差不多相同的时间,我们的 Web API 规范中有了 Fetch API。至今已经十多年,Fetch API 已经成为现代 Web 开发的标配,成为支持最广泛的 API 之一。另一方面,在大型项目中,定制化的需求越来越多(很多可能是出于代码风格的要求或者和后端团队奇怪的约定),团队本来就会做一层统一 HTTP abstraction,那 Axios 当年提供的很多“开箱即用价值”,今天已经有一部分被 Fetch + 自家封装吃掉了。现代大型前端项目,常常内部本来就有统一请求层,需要做 snake_case ↔ camelCase、鉴权头注入、错误归一化、业务码处理等等各种各样的定制化需求,Axios 的一些功能在这种场景下就不那么有吸引力了。除了极少数的场景 Fetch API 因为抽象层次的问题能力上还不太完善,大多数时候,我们把抽象封装沉淀在项目里或者内部工具项目中,使用 Axios 可能反而带来更沉重的运行时复杂度 —— 我们在对着一个已经很重的封装层做封装。
很遗憾的是我所能接触到的生产项目几乎都还在使用 Axios —— 甚至有的是我亲手进行的技术选型,这就是历史惯性的力量吧。我暂时没有动力对它们进行迁移。
至于供应链安全 —— 减少一个依赖总归是对安全性有帮助的,对吧?