axios 中毒:一次不改源码的 npm 投毒,给整个 JavaScript 世界敲了警钟

一次“没改源码”的投毒,却比改源码更可怕
如果你是前端、Node.js 开发者,axios 这个名字几乎不需要介绍。它就像 JavaScript 世界里的自来水阀门:你请求接口、拉数据、调服务,十有八九绕不过它。也正因为太常见,它一旦出事,影响就不是“一个库坏了”,而是半个生态都要跟着抖三抖。
StepSecurity 披露的信息显示,2026 年 3 月 31 日,npm 上出现了两个被投毒的 axios 版本:axios@1.14.1 和 axios@0.30.4。攻击者并没有粗暴地往 axios 源码里塞后门,而是用了更隐蔽、也更现代的一招——新增一个根本不会被项目真正调用的依赖 plain-crypto-js@4.2.1,专门借助 postinstall 脚本在安装时下发远控木马(RAT)。
这种手法之所以阴险,在于它利用了开发者最松懈的时刻:npm install。大家装依赖时通常不会把每个子依赖翻开看,更不会想到一个大名鼎鼎的热门包,危险不在主代码,而在安装动作本身。你看到的是“升级 axios 小版本”,攻击者看到的是“把木马送进成千上万台开发机、CI 机器和服务器”的黄金通道。
攻击者不是在试水,而是在“做项目”
看完整个攻击时间线,我最大的感受不是“黑客真狡猾”,而是“这帮人很专业”。他们并不是临时起意,往 npm 上丢个恶意包碰碰运气,而是像做一场精心排练的演出。
在真正污染 axios 之前,攻击者先用一个临时账号发布了 plain-crypto-js@4.2.0,内容干净,伪装成正常的 crypto-js 变体,只为给这个包建立“历史记录”。18 小时后,再发布带有恶意 postinstall 的 4.2.1。这一步很有意思:很多安全工具会盯着“刚发布、没历史、作者陌生”的包,但如果一个包已经有了前序版本,看起来就没那么扎眼。说白了,这像是先办了一张假身份证,再拿它去银行开户。
更关键的是,这次攻击不是从 GitHub 仓库的代码流程打进去的,而是直接利用了 axios 主要维护者的 npm 账号凭证。正常的 axios 1.x 发布,本来是通过 GitHub Actions 加上 npm 的 OIDC Trusted Publisher 机制完成,发布链路可验证、令牌也短时有效,理论上不容易被窃取。可这次的恶意版本跳过了整套 CI/CD 流程,直接用被盗的长期 npm token 手工发布,连对应的 GitHub commit 和 tag 都不存在。换句话说,这不是代码仓库失守,而是“钥匙被偷了,贼直接从正门进来”。
这一点非常值得行业警惕。过去几年,大家一直在强化代码审查、分支保护、自动化测试,但很多项目对包管理平台账号本身的保护仍然不够。现实很残酷:你把仓库门装成防弹玻璃,不代表家门钥匙就不会丢。
真正的危险,藏在依赖树最不起眼的角落
这次投毒最经典的一点,是它几乎没有碰 axios 业务逻辑。两个恶意版本与前一个干净版本相比,差异非常“克制”:只是多了一个运行时依赖 plain-crypto-js@^4.2.1。除此之外,其它依赖都没变。一个小改动,换来的是跨平台木马投递能力。
更夸张的是,这个依赖在 axios 的源码里根本没有被 import 或 require() 过。它存在的唯一意义,就是触发安装阶段的 postinstall。这相当于有人往你的外卖里塞了一把不属于这道菜的钥匙,而你只有在门已经被打开后,才意识到这把钥匙根本不该出现。
从技术细节看,这个名为 setup.js 的安装脚本也不是随便写写。它做了双层混淆,隐藏系统判断、C2 地址、shell 命令和文件路径,运行后会按操作系统下发不同的二阶段载荷:在 macOS 上伪装成系统缓存目录里的可执行文件,在 Windows 上用 VBScript 和 PowerShell 悄悄落地,在 Linux 上则下载 Python 脚本后台运行。三套投递链路提前准备好,共用一个指挥控制服务器。
这类设计说明攻击者对开发环境很熟。开发机不只是“写代码的电脑”,往往还存着云平台密钥、SSH 凭证、CI token、生产环境访问权限、企业 VPN、私有 npm 源账号,甚至浏览器里还开着各种后台。攻下一台开发机,有时比攻下一台普通办公电脑值钱得多。过去不少供应链攻击——从 event-stream 到 ua-parser-js,再到 3CX 事件——都在提醒同一个现实:软件供应链最贵的不是代码本身,而是代码背后连着的信任网络。
最让人后背发凉的,是它会“擦屁股”
很多恶意 npm 包并不高明,安装之后一查目录,证据基本都在。但这次不一样。plain-crypto-js 在执行完以后,会删除自己的 setup.js,再把带 postinstall 的 package.json 删掉,最后用预先准备好的干净 package.md 重命名回 package.json。也就是说,当你事后去翻 node_modules,看到的可能是一个“没有任何异常”的包。
这招非常像职业犯罪里的清理现场:门锁看着完好,地面拖得很干净,监控还被覆盖了。对于很多企业来说,这比直接留后门更麻烦,因为它会误导排查方向。开发者可能会以为“我检查过依赖目录,没发现 postinstall 啊”,但实际上,证据已经被程序自己抹掉了。
所以 StepSecurity 才给出了相当强硬的结论:只要你装过 axios@1.14.1 或 axios@0.30.4,就应该默认机器已经失陷。不是“建议检查一下”,而是“按被攻破处理”。这类判断听起来刺耳,但在响应供应链攻击时,宁可多算,也不能少算。因为一旦远控已经跑起来,损失往往不止这一台机器,而是这台机器曾经接触过的所有系统、密钥和内部网络。
这件事真正刺痛行业的地方:默认信任,可能到了该改的时候
为什么这次事件格外重要?因为它打中了现代软件开发最脆弱、也最难彻底修补的一环:我们对开源依赖的默认信任。
JavaScript 生态的繁荣,建立在极低的复用门槛上。一个项目动辄上千个包,开发者很难逐个审计,只能相信热门项目、相信维护者、相信包管理器、相信自动化流程。这种信任平时让创新跑得飞快,但一旦被利用,破坏力也会被同样的网络结构瞬间放大。axios 每周下载量超过 3 亿次,这意味着哪怕只有极小一部分用户在短窗口期内升级到恶意版本,受影响面都可能非常可观。
更值得思考的是,像 postinstall 这种机制,到底该不该继续被如此宽松地默认启用?它确实解决了部分原生模块编译、环境准备、安装后配置的问题,但也长期是恶意包最爱的入口之一。过去几年,社区已经越来越频繁地讨论“最小化安装脚本权限”“对未使用依赖做高风险提示”“将发布来源验证默认展示给用户”等方向。axios 这次中毒,很可能会把这些讨论再次推上台面。
在我看来,未来的开源安全不会只拼“谁扫描得更快”,而是拼谁能把信任做成分层结构。比如:维护者强制硬件密钥和多重验证、npm 侧默认高亮非 OIDC 发布、企业内部对高危 postinstall 做拦截、CI 环境把依赖安装与密钥访问隔离开来、SBOM 和依赖行为分析成为默认配置。开发者不可能手查每一行代码,但平台至少应该把“哪里不符合历史模式”这件事大声说出来。
对普通开发团队来说,现在最实际的动作也很明确:如果装过这两个版本,立刻回退到 axios@1.14.0 或 axios@0.30.3,并轮换受影响机器上的所有凭证,检查到可疑域名和相关 C2 的网络连接记录。别把它当成一次普通的依赖升级事故,它更像是一场悄无声息的办公室入侵。
说到底,这不是 axios 一家的耻辱,而是整个开源世界的集体压力测试。只是这次,考卷来得有点太突然了。