Axios 被投毒:一场教科书级的供应链攻击复盘
Posted on 三 01 4月 2026 in Tech
| Abstract | Axios 被投毒:一场教科书级的供应链攻击复盘 |
|---|---|
| Authors | Walter Fan |
| Category | Journal |
| Version | v1.0 |
| Updated | 2026-04-01 |
| License | CC-BY-NC-ND 4.0 |
短大纲
- 以 "愚人节清晨收到安全告警" 的真实场景开篇,引出 axios 投毒事件
- 攻击手法拆解:维护者账号劫持 → 注入恶意依赖 → postinstall 下载木马 → 跨平台 RAT
- 攻击者的"细节控":双层混淆、自毁痕迹、伪装系统进程
- 你中招了吗?检查清单与应急响应步骤
- 防护思路:lockfile、npm ci、--ignore-scripts、依赖审计
- 总结 + 思维导图 + 可执行清单 + 开放式问题
2026-04-01
愚人节清晨,我在翻安全资讯时看到一条消息,差点以为是恶作剧:axios——那个周下载量超过 1 亿次的 npm 包——被投毒了。
前几天刚写完 LiteLLM 供应链投毒事件的复盘——PyPI 上两个恶意版本把用户的 API Key、SSH 密钥、云凭证一锅端走——墨迹未干,这边又出事了。一波未平,一波又起。上周是 Python 生态的 LiteLLM,这周轮到 JavaScript 生态的 axios。手法如出一辙:劫持维护者账号,注入恶意依赖,窃取凭据或植入后门。树欲静而风不止,AI 辅助编程让我们的开发效率翻了好几倍,可黑客也在用同样的节奏加速攻击——你写代码的速度在提升,供应链被攻陷的频率也在提升。
仅仅两个小时的窗口期内,任何跑了 npm install 的机器,都可能被植入了一个跨平台的远控木马(RAT, Remote Access Trojan)。不是 typosquatting(拼写仿冒),不是某个不知名的小包,而是 axios 本身。你的项目八成在依赖列表里就有它。
这篇文章不是为了制造恐慌——毕竟恶意版本已经被下架。但结合 LiteLLM 事件一起看,这波供应链攻击的密度和精度都在升级,值得拿来做一次系统性的复盘。看完你会知道三件事:攻击者怎么干的、你该怎么检查自己有没有中招、以及以后怎么防。
事件概览:两小时的窗口期
| 关键信息 | 详情 |
|---|---|
| 受影响版本 | axios@1.14.1、axios@0.30.4 |
| 根因 | 维护者 npm 账号被劫持 |
| 恶意依赖 | plain-crypto-js@4.2.1 |
| 载荷 | 跨平台 RAT(macOS / Windows / Linux) |
| C2 服务器 | sfrclak[.]com:8000(IP: 142.11.206.73) |
| 发布时间 | 2026-03-31 00:21 UTC(1.14.1);01:00 UTC(0.30.4) |
| 下架时间 | 2026-03-31 03:29 UTC |
| 窗口期 | 约 2-3 小时 |
整个事件的时间线让人倒吸一口凉气:从投毒到被发现只用了 6 分钟(Socket 的扫描器率先报警),但从发现到最终下架,走完 npm 的流程花了将近 3 小时。这 3 小时里,全球不知道有多少条 CI/CD 流水线跑了 npm install。
攻击手法拆解:一个教科书级的操作
这次攻击的精妙之处在于:攻击者一行 axios 的源码都没改过。
第一步:劫持维护者账号
攻击者拿到了 axios 的一个核心维护者 @jasonsaayman 的 npm 发布权限。据 GitHub issue 里的讨论,这个账号的权限比其他协作者更高,导致事发后其他维护者很难快速响应。
这是经典的 "打蛇打七寸"——不攻代码仓库,攻 人。你的密码强度再高,如果没有开 2FA 或者 token 泄露了,一切白搭。
2FA 是 Two-Factor Authentication(双因素认证)的缩写。 普通登录只需要一个因素——密码。2FA 要求你在输入密码之后,再提供第二个验证因素,通常是以下之一: TOTP 动态码:手机上的 Authenticator App(如 Google Authenticator、1Password)每 30 秒生成一个 6 位数字 短信验证码:发到你手机上的一次性码(安全性较低,容易被 SIM swap 攻击) 硬件安全密钥:比如 YubiKey,插到 USB 口或 NFC 感应 道理很简单:密码可以被钓鱼、被撞库、被泄露,但攻击者同时拿到你的密码和你手机上的动态码就难多了。
回到这次 axios 事件——维护者的 npm 账号被劫持,很可能就是因为密码泄露或被钓鱼,而又没有开启 2FA。npm 已经对下载量最高的一批包强制要求维护者开启 2FA,但显然覆盖面还不够。开了 2FA 不是万能的,但没开基本等于把家门钥匙挂在门把手上。
第二步:注入隐蔽的恶意依赖
拿到发布权后,攻击者没有修改 axios 的任何源文件(这样 diff 里看不出异常),而是在 package.json 里悄悄加了一个新依赖:plain-crypto-js@4.2.1。
更狡猾的是,这个恶意包不是临时注册的——它在 18 小时前就发了一个 "干净版" 4.2.0,让包看起来有一点历史记录。等到要下手了,再推一个带毒的 4.2.1。
这招叫 "先养号,再出刀"。就像钓鱼邮件用的那些看上去很正常的发件地址一样,先建立一点 "信用",再动手。
第三步:postinstall 里的暗桩
plain-crypto-js@4.2.1 的 package.json 里有一行:
"scripts": {
"postinstall": "node setup.js"
}
跑 npm install 的时候,npm 会自动执行 postinstall 脚本——这是 npm 的 设计特性,不是 bug。大量正经包(比如编译 native addon)都用这个钩子。但它也是供应链攻击最爱的入口。
第四步:双层混淆 + 自毁
setup.js 里的代码做了两层混淆:
- 反转 Base64:先把 Base64 字符串反转,再替换 padding 字符
- XOR 加密:用密钥
OrDeR_7077加上常数333做 XOR
解混淆后,脚本会根据 os.platform() 判断操作系统,然后去 C2 服务器下载对应平台的第二阶段载荷。
下载完成后,它会把 setup.js 删掉,把包含 postinstall 钩子的 package.json 也删掉,再用一个干净的 package.md(重命名为 package.json)替换上去。事后你去 node_modules/plain-crypto-js 里翻,什么可疑痕迹都看不到。
这不是脚本小子能搞出来的东西。
三个平台,三种木马
攻击者为每个平台量身定做了 RAT(Remote Access Trojan 远程访问木马)。顾名思义,RAT 是一种恶意软件,安装到受害者机器后,攻击者可以像坐在那台电脑前一样远程操控它——执行命令、读写文件、截屏、窃取凭据,基本上想干嘛干嘛。和普通木马的区别在于,RAT 强调的是持续的、交互式的远程控制能力,而不只是一次性偷个文件就跑。
macOS:冒充 Apple 守护进程
下载一个二进制文件到 /Library/Caches/com.apple.act.mond——故意模仿 Apple 后台进程的命名。这个 RAT 会:
- 生成 16 位唯一 ID 标识受害机器
- 收集系统信息:主机名、用户名、macOS 版本、CPU 架构、运行进程
- 每 60 秒向 C2 心跳(用了一个假冒 IE8/Windows XP 的 User-Agent,混淆流量分析)
- 接受远程命令:
peinject:下载并执行任意二进制,还会用codesign --force --deep --sign -做临时签名绕过 Gatekeeperrunscript:执行 shell 命令或 AppleScriptrundir:扫描/Applications、~/Library等目录kill:自毁
C2 是 Command and Control(命令与控制)的缩写,指的是攻击者用来下发指令和接收窃取数据的服务器。你可以把它理解为木马的"大脑"或"遥控器":
- RAT 安装后会定期向 C2 服务器"报到"(心跳),告诉攻击者"我还活着"
- 攻击者通过 C2 服务器向 RAT 发送指令(比如文中提到的 peinject、runscript 等)
- RAT 把执行结果或窃取的数据回传给 C2
在这次 axios 事件中,C2 服务器是 sfrclak[.]com:8000(IP: 142.11.206.73)。RAT 每 60 秒连一次这个地址,等待攻击者的命令。所以检测是否中招的一个关键手段就是看网络日志里有没有到这个地址的外联请求。
用一个比喻来说:RAT 是潜伏在你电脑里的间谍,C2 是间谍接头的秘密据点。
Windows:PowerShell 马甲
用 VBScript 把 powershell.exe 复制到 %PROGRAMDATA%\wt.exe(伪装成 Windows Terminal),然后以隐藏窗口、绕过执行策略的方式启动 PowerShell RAT。
Linux:Python 后台进程
下载 Python 脚本到 /tmp/ld.py,用 nohup python3 以孤儿进程方式启动,脱离终端 session。
可以看出,攻击者不是随便写了个脚本就完事——每个平台的持久化策略和反检测手段都做了针对性设计。
你中招了吗?快速自检
先别慌,跟着这个清单过一遍:
1. 检查 lockfile
# npm
grep -E '"axios"' package-lock.json | grep -E '1\.14\.1|0\.30\.4'
# yarn
grep -E 'axios@' yarn.lock | grep -E '1\.14\.1|0\.30\.4'
# bun
grep -E 'axios' bun.lock | grep -E '1\.14\.1|0\.30\.4'
如果没匹配到,基本没事。lockfile 是你的第一道防线——如果它在投毒前就锁定了版本,npm install 不会去拉新版本。
2. 检查恶意依赖
npm ls plain-crypto-js
如果输出 empty,安全。
3. 检查系统 IOC(如果怀疑已执行过)
| 平台 | 检查项 |
|---|---|
| macOS | 文件 /Library/Caches/com.apple.act.mond 是否存在 |
| Windows | 文件 %PROGRAMDATA%\wt.exe 是否存在 |
| Linux | 文件 /tmp/ld.py 是否存在 |
| 网络 | 是否有到 sfrclak[.]com 或 142.11.206.73:8000 的外联 |
4. 如果确认中招了
假设已被完全攻破,按以下步骤应急:
- 隔离受感染机器——断网、下线
- 轮换所有凭据——不是改密码就行,是 吊销并重新签发 所有 API Key、SSH Key、npm token、云凭据。在受感染机器上改密码等于把新密码也送给攻击者
- 检查横向移动——翻日志看有没有到 C2 的外联记录,有的话说明 RAT 已经活动过
- 重建环境——不要试图 "清理",从干净的快照或基础镜像重建
- 审计 CI 流水线——检查 3 月 31 日 00:21 - 03:29 UTC 窗口期内的构建日志
防护:不是不用 npm,而是要用对
这次事件暴露的不是 npm 的 "设计缺陷",而是我们对依赖管理的 默认信任 太多了。
lockfile 是你的生命线
# CI 里永远用 npm ci,不用 npm install
npm ci
npm ci 严格按 package-lock.json 安装,不会自动解析新版本。如果 lockfile 在投毒前提交且 CI 用 npm ci,这次攻击对你完全无效。
禁用 postinstall(适用于 CI)
npm ci --ignore-scripts
这会阻止所有生命周期脚本执行。代价是某些需要编译 native addon 的包(如 sharp、bcrypt)会失效,需要单独处理。但在纯 JS/TS 项目的 CI 环境里,这是性价比极高的防护。
依赖审计要常态化
# npm 内置
npm audit
# 或用 Snyk 等第三方工具
npx snyk test
把依赖审计加进 CI 流程,不通过就不让合并。
维护者账号安全
这个事件本质上是 人 的账号被攻破。对于维护核心开源包的人来说:
- 开 2FA——npm 已经强制要求 top-500 包的维护者开启
- 用 publish token 限制 IP 和包名——不要用 all-access token
- 定期审查谁有发布权限——离开的成员要及时移除
对于使用者来说,除了以上技术手段,还可以考虑:
供应链安全的本质问题
这不是第一次了。ESLint 的 Prettier 插件被投过毒、Shai-Hulud 事件一次攻陷了 600 多个包、event-stream 事件至今还是经典案例。每次事后大家都说 "要重视供应链安全",然后等热度过了又恢复原样。
问题的根源在于:开源生态的信任模型是基于 "人" 的,但 "人" 恰恰是最容易被攻破的环节。 一个维护者的密码被钓鱼、一个 token 写在了 .env 里被推到了公开仓库、一个前维护者心怀不满——任何一种情况都可能让上亿用户受影响。
技术层面,我们能做的是 减小信任面:
- lockfile 锁定版本(不信任 registry 上的新版本)
--ignore-scripts禁用脚本(不信任包的安装钩子)- 最小权限原则管理 token 和账号
- 构建环境网络隔离(不让构建机器随意外联)
- 运行时监控异常进程和网络连接
总结
@startmindmap
* Axios 供应链攻击复盘
** 攻击手法
*** 维护者账号劫持
*** 注入恶意依赖 plain-crypto-js
*** postinstall 执行恶意脚本
*** 双层混淆 + 自毁痕迹
*** 跨平台 RAT(macOS/Win/Linux)
** 影响范围
*** 窗口期:~3 小时
*** 波及所有未锁版本的项目
*** CI/CD 流水线风险最大
** 自检手段
*** 检查 lockfile 有无 1.14.1/0.30.4
*** npm ls plain-crypto-js
*** 检查系统 IOC
*** 审计构建日志
** 防护措施
*** 提交 lockfile + npm ci
*** CI 中 --ignore-scripts
*** 依赖审计常态化
*** 维护者 2FA + token 最小权限
*** 构建环境网络隔离
** 深层思考
*** 开源信任模型基于"人"
*** "人"是最薄弱的环节
*** 核心策略:减小信任面
@endmindmap

检查清单
- 检查你的 lockfile:确认没有
axios@1.14.1或0.30.4 - CI 改用
npm ci:如果还在用npm install,现在就改 - 评估
--ignore-scripts:在不需要 native addon 的项目里启用 - 开启 npm 2FA:特别是你维护的包被别人依赖的话
- 把
npm audit加进 CI:不通过就不让合并
什么时候该用、什么时候别用这些措施?
npm ci适用于所有 CI 环境,本地开发如果频繁加新包则仍用npm install,但记得 提交 lockfile--ignore-scripts在纯 JS/TS 项目的 CI 里几乎零成本,但如果项目依赖node-gyp编译的包(如canvas、sqlite3),需要单独跑 rebuild- 依赖审计在所有项目里都该做,但要合理配置忽略规则,避免被大量低风险告警淹没
一个开放式问题
npm 现在对 top-500 包强制 2FA,但 axios 排名远在这个范围内——那为什么维护者账号还是被攻破了?是 2FA 被绕过了,还是根本没有强制生效?如果连 axios 这种级别的包都保护不了,我们对 npm 生态的信任还能建立在什么之上?
参考链接
- Axios npm Package Compromised: Supply Chain Attack Delivers Cross-Platform RAT - Snyk Blog
- Snyk Advisory: SNYK-JS-AXIOS-15850650
- npm security best practices
- npq - Safely install packages with npm/yarn
- Socket - Supply chain security for npm
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。