当前位置:
AIGC文章详情

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

源自146位全网作者

15:21

8月4日,一个你可能从没主动安装过、但八成已经躺在你依赖树里的npm包出事了。

常用缓存库 keyv 的维护者GitHub账号被攻破,攻击者借着完全合法的发布流程,把带毒版本推上了npm。知乎安全公司 Snyk 第一时间独立复盘了整件事,结论很直接:这不是一次性投毒,恶意程序会拿着偷来的npm凭据继续感染你名下的其他包,像蠕虫一样扩散。知乎你跑ESLint、跑CI、用任何基于cache-manager的工具,都可能已经绕进这条依赖链。

先说结论,方便你决定要不要往下读:如果你或你的CI在8月4日之后安装过受影响版本,先别急着换密钥,按下面的顺序处理,顺序错了可能多挨一下。

到底发生了什么

Snyk 安全团队没有安装包、只对比了registry上的原始tarball,确认的事实是这样的。Snyk

攻击者先控制了维护者 Jared Wray 的GitHub身份,往 keyv、cacheable、ecto 三个仓库塞入恶意文件,再走正常的GitHub Actions发布流程,把 keyv@6.0.0 推上npm。知乎这个版本带了一个 `preinstall` 钩子——你根本不用import keyv、不用启动应用,只要 `npm install` 解析到它,钩子就会在安装前自动跑。

钩子先执行一个约30KB的 `setup.mjs`,它会静默下载合法的Bun运行时(v1.3.13),再用Bun去跑真正的第二阶段载荷 `Math_Symbol.js`(约728KB)。知乎这个载荷会翻找你环境里的 npm、GitHub、AWS、GCP、Azure、Vault、Kubernetes 凭据,连 .env、SSH私钥、Terraform、Docker 配置、甚至 Claude / Cursor / Copilot 这些AI工具的配置文件都不放过。知乎

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

更麻烦的是,偷到npm token后,它会枚举这个账号能发布的所有包,注入同一套钩子、抬高版本号、再发布出去。知乎这就是它能从几个包扩散到几百个包的原因。

三个数字,别混为一谈

网上这轮信息很乱,同一个事件有三个口径,值得分清:

11个:Snyk 亲自确认的直接恶意版本。Snyk 都在维护者 jaredwray 名下,包括 keyv@6.0.0、flat-cache@6.1.24、file-entry-cache@11.1.6、cacheable-request@13.0.20、cache-manager@7.2.10、cacheable@2.5.1、ecto@5.0.1,以及 @cacheable/net、@cacheable/node-cache、@cacheable/memory、@cacheable/utils。这些是铁证,清单固定。

444个包、2212个版本:蠕虫第二波扩散的战果。 来自安全厂商对 Shai-Hulud 2.0 蠕虫的追踪——偷来的凭据被用来感染了十多个组织名下的几百个包。知乎这是"已确认受影响"的规模,清单还在更新。

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

868个:目前不可信。 最早那条推文写"至少868个包",但没附清单,也对不上几家安全公司的公开记录。知乎这个数字现阶段不能当事实引用,看到它打个问号。

还有一个容易误读的:有厂商海报写"每月20亿次下载"。这说的是这几个包扎在多大的依赖树里、爆炸半径有多大,不是感染了多少台机器、更不是多少项目已经中招。知乎把它当成"波及面"理解,别当成"受害数"。

你没装过keyv,为什么也可能中招

因为keyv往往不是你自己装的,是被工具链带进来的。最常见的链路是:

`eslint → file-entry-cache → flat-cache → keyv`

Wiz 对客户环境的统计里,file-entry-cache、flat-cache、keyv 的出现率都在 46%左右知乎也就是说,将近一半的云与代码环境里都有它们。这就是这次扩散这么快的根本原因——你装的是ESLint,绕着绕着就绕进了keyv。

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

一个反常识:这次"合法签名"没救场

keyv@6.0.0 是带 verified provenance(可信来源签名) 的,因为它确实是那条正常的GitHub Actions工作流构建出来的。Snyk问题在于:工作流拿到的源代码,已经被污染了。

签名能证明"是谁、用哪条流水线构建的",但证明不了"源代码本身是不是干净"。知乎这次事件把这个边界划得清清楚楚——provenance 依然是有价值的证据,但它不是免死金牌。以后看到绿勾,别急着放心。

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

怎么自查:看lockfile,别只看package.json

很多人第一反应是去翻 package.json,这没用——你可能根本没直接声明keyv。真正要查的是 lockfile最终解析到了哪个版本,以及 安装钩子到底有没有在开发机或CI上跑过

先把整条依赖链查出来(注意加 `–all` 看传递依赖):

```bash
npm ls keyv flat-cache file-entry-cache cacheable-request cacheable
@cacheable/utils cache-manager @cacheable/net
@cacheable/node-cache @cacheable/memory ecto --all
```

再扫lockfile——npm改了dist-tag之后,坏版本可能还被锁在里面:

```bash
rg -n ‘keyv|flat-cache|file-entry-cache|cacheable-request|cacheable|cache-manager|@cacheable/|ecto’
package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock
```

然后不执行任何包代码,直接翻已安装的 manifest,找那个特征钩子:

```bash
find node_modules -name package.json -print0 | xargs -0 node -e ’
const fs=require(“node:fs”);
for(const f of process.argv.slice(1)){
try{
const p=JSON.parse(fs.readFileSync(f,“utf8”));
if(p.scripts?.preinstall===“node setup.mjs”){
console.log(`${p.name}@${p.version} ${f}`);
}
}catch{}
}’
```

最后找载荷和持久化的痕迹:

```bash
find “$HOME” /tmp
( -name setup.mjs -o -name Math_Symbol.js -o -name math_init.js
-o -name gh-token-monitor.sh -o -name gh-token-monitor.service
-o -name com.user.gh-token-monitor.plist ) -print 2>/dev/null
```

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

判断标准其实就一句话:恶意版本有没有进过lockfile、安装钩子有没有真的执行、仓库启动项有没有被写入。知乎 三个都干净,才算没中招;只要运行过恶意版本,就按"主机已失陷"处理,不是升个级就完事。

处置顺序,这里是最反常识的地方

如果你确认中招了,第一反应多半是"赶紧把密钥全换了"。先别。

这次恶意程序埋了一个密钥失效触发器:Socket 发现它会每60秒检查一次被盗的GitHub token还有效没,一旦收到4xx——也就是你把token撤销或轮换了——它可能会执行一段远程下发的处理命令。知乎你急着换钥匙,等于亲手触发它的下一步动作。

所以没有事件响应经验的团队,最稳的顺序是:

  1. 先断网隔离主机,保住日志、进程、npm日志、CI输出、文件系统时间戳这些证据;

  2. 让安全人员清掉持久化项再去碰凭据。重点查这几个位置:`~/.local/bin/gh-token-monitor.sh`、`~/.config/gh-token-monitor/`、`~/.config/systemd/user/gh-token-monitor.service`、`~/Library/LaunchAgents/com.user.gh-token-monitor.plist`。注意,撤销动作对监控程序可能是"可见"的,先拆监控再换钥匙;

  3. 在一台确认干净的机器上轮换凭据——GitHub PAT和App token、npm token、AWS/GCP/Azure、Vault、Kubernetes、数据库、私钥,凡是那次CI任务能拿到的都要换。然后回头查账号和云的审计日志,找异常的npm发布、新建仓库、workflow改动、陌生地点的token使用。

别忘了私有镜像和缓存:npm把版本删了,不代表你内网代理、本地缓存里那份也消失了,要主动清。

没中招的,现在就该把防线补上

把已知干净的版本用 `overrides` 钉死,别让安装脚本有机会跑:

```json
{
“overrides”: {
“keyv”: “5.6.0”,
“flat-cache”: “6.1.23”,
“file-entry-cache”: “11.1.5”,
“cacheable-request”: “13.0.19”,
“cacheable”: “2.5.0”,
“@cacheable/utils”: “2.5.0”,
“cache-manager”: “7.2.9”,
“@cacheable/net”: “2.1.0”,
“@cacheable/node-cache”: “3.1.1”,
“@cacheable/memory”: “2.2.0”,
“ecto”: “5.0.0”
}
}
```

然后重建lockfile,全程禁掉生命周期脚本:

```bash
npm install --package-lock-only --ignore-scripts
rm -rf node_modules
npm cache clean --force
npm ci --ignore-scripts
```

日常层面,三件事值得做:装依赖默认 `–ignore-scripts`,只给确实需要安装脚本的包开白名单;把 `snyk test` 和 `snyk monitor` 挂进CI,这次 Snyk 已经发布了 keyv@6.0.0 的专项公告(编号 SNYK-JS-KEYV-18515941),重跑一遍就能对上。Snyk重要项目定期人工过一眼lockfile里的高危传递依赖。这些对个人和开源项目基本都是免费的,性价比很高。

Snyk确认11个keyv恶意版本:藏在eslint的传递依赖里,近半环境在波及范围——先自查再处置,但千万别先换密钥

截至今天(8月26日)的最新状态

事件还在演进,几个你该知道的现状:

  • npm已经把 keyv@6.0.0 下架,`latest` 回退到干净的 5.6.0npm官方registry也就是说现在全新 `npm install` 默认不会再装到那个毒版本,但你的lockfile和私有缓存仍需自查

  • 干净的6.x后继版本还没发布,目前只有alpha/beta/rc。Snyk从6.0.0退回5.x可能需要改点代码,生产环境优先用5.6.0这个稳的,别去赌rc。

  • 维护者正在恢复:就在今天,ecto 发布了一个新的 5.1.0。npm官方registry不过账号曾被攻破,对这类"恢复后的新版本"建议先观望、核对公告再升级,别急着信。

一句话总结

这次 keyv 投毒真正的教训不是"某个包不安全",而是:你最信任的合法发布流程,也可能被人借走;而你的安全边界,往往藏在那些你从没注意过的传递依赖里。 自查命令收好,处置顺序记牢——先隔离、再拆监控、最后才换钥匙。

要不要我帮你把这套自查命令整理成一份可以直接丢进CI的检查脚本?

内容由AI生成
0
扫一下,分享更方便,购买更轻松
0评论

当前文章无评论,是时候发表评论了
提示信息

取消
确认
评论举报

最新文章 热门文章