一句「npm publish」评论,撬开一条可信发布流水线:openapi-react-query-codegen 供应链投毒事件深度复盘
原创 威胁情报中心 2026-08-31 15:45 北京

十个恶意版本、约 15 万周下载量、全部携带有效的 npm 来源证明(provenance)——这一次,攻击者甚至没有偷任何人的账号密码,只是在 Pull Request 下面留了一句评论。
供应链安全深度复盘
一句「npm publish」评论,撬开一条可信发布流水线
openapi-react-query-codegen 供应链投毒事件深度复盘:十个恶意版本、约 15 万周下载量、全部携带有效的 npm 来源证明(provenance)——这一次,攻击者甚至没有偷任何人的账号密码,只是在 Pull Request 下面留了一句评论。
· 供应链安全 · npm 生态 · 2026-08-31
01 事件速览
2026 年 8 月 28 日,npm 生态再次发生供应链投毒事件。中招的是 @7nohe/openapi-react-query-codegen——一个能将 OpenAPI 规范自动生成 TypeScript 客户端和 React Query hooks 的代码生成工具,全版本周下载量约 15 万次,在 React 开发者群体中应用广泛。
当天 UTC 时间 20:00 至 20:21 之间,攻击者分两波(间隔约 20 分钟)向 npm 发布了 10 个恶意版本,覆盖了该包所有仍在维护的版本线。安全研究团队 Socket 与 StepSecurity 先后披露了此事,并将其与 Mini Shai-Hulud 蠕虫家族关联分析。
项目 | 内容 |
受影响包 | @7nohe/openapi-react-query-codegen |
恶意版本数 | 10 个(8 个稳定版 + 2 个 0.0.0-* 预发布版) |
发布窗口 | 2026-08-28 20:00:43 – 20:20:53 UTC |
周下载量 | 约 150,000 |
攻击入口 | GitHub Actions 中由 PR 评论触发的发布工作流 |
载荷特征 | 凭证窃取 + 多注册表自我传播,具备蠕虫属性 |
已知安全版本 | 0.5.3、1.6.2、2.2.0、3.0.2 |
截至本文发稿(8 月 31 日),npm 注册表已移除全部 10 个恶意版本,latest 标签已重新指向安全版本 3.0.2。但在事件曝光初期,latest 一度直接解析到恶意版本 3.0.4——这意味着那段时间里一条最普通的 npm install 就会把木马装进开发机或 CI 环境。

图 | 事件曝光初期,npm 页面上latest标签指向恶意版本 3.0.4(图:StepSecurity)
02 攻击入口:一句评论如何变成「发布许可证」
这起事件最值得复盘的不是恶意代码本身,而是攻击者进入发布流水线的姿势——它简单到令人不安。
2.1 被滥用的工作流
该项目的 release.yml 发布工作流监听 issue_comment 事件,触发条件大致是:
if: ${{ github.event_name == 'push' || (github.event.issue.pull_request && github.event.comment.body == 'npm publish') }}
翻译成大白话:只要某条评论出现在 PR 下面,且内容恰好是「npm publish」,发布任务就会启动。 整个判断逻辑里没有任何一步去核验评论者的身份——是不是仓库成员、是不是协作者、有没有写权限,一概不问。
更致命的是这个任务的后续动作:
1检出该 PR 的头部代码(pull/
2pnpm install 安装依赖;
3以 id-token: write 权限运行 pnpm publish --no-git-checks,通过 npm Trusted Publishing 的 OIDC 身份直接发包。

图 | release.yml 顶部:任务持有 id-token: write,触发条件只看评论文本是否等于「npm publish」(图:StepSecurity)

图 | 同一工作流中,任务检出 PR 头部代码后执行 pnpm install 与 pnpm publish --no-git-checks(图:StepSecurity)
于是,任何一个 GitHub 路人账号,只要 fork 仓库、提一个塞满恶意代码的 PR、再在评论区敲下「npm publish」,就能以该项目官方的可信发布身份把代码推到 npm 上。 不需要 maintainer 密码,不需要窃取长期 token,甚至不需要任何人点「合并」。整个攻击入口的完整时序如下图所示:

图 | 攻击入口完整时序
2.2 攻击者的实际操作
GitHub 账号 p00paboot 正是这么干的:8 月 28 日 19:59 UTC,它提交了 PR #215(标题伪装成「Add new testcases for OpenAPI」),随后在同一条 PR 下发出「npm publish」评论,紧接着又操作了 PR #216。工作流运行畅通无阻地走到了发布步骤,npm 注册表的时间戳在几秒内跟进。整个过程在公开页面上完成,没有任何入侵痕迹可言——因为大门本来就是敞开的。

图 | 攻击者 fork 中的恶意提交 365d4eb:向 package.json 注入带环境变量的 preinstall 命令(图:Socket)
最先发现异常并报告的是安全研究员 Charlie Eriksen(GitHub issue #217)。
03 来源证明(Provenance)为什么没能拦住恶意包
这是本次事件最具警示意义的一环。
npm 的 provenance 机制配合 GitHub Actions Trusted Publishing,会用 OIDC 身份为发布的包生成加密签名证明,npm audit signatures 可以校验。而这次全部 10 个恶意版本都携带了有效的 provenance 证明——因为它们确实是通过项目自己的 release.yml、以项目的官方 OIDC 身份发布的。签名是真的,流水线是真的,只有代码是假的。
这里有一个微妙但关键的细节:issue_comment 事件触发的工作流中,GITHUB_REF 取的是默认分支。因此恶意包的 SLSA 证明里记录的是 refs/heads/main 上最后一个正常提交(即干净的 v3.0.2 提交 d42d1733)——证明文书看上去完美无瑕,而 tarball 里装的却是攻击者的代码。

图 | 恶意版本 3.0.4 在 npm 页面上展示着有效的 provenance:构建于 GitHub Actions,源提交指向干净的d42d173(图:StepSecurity)
Provenance 证明的是「这个产物由哪条流水线构建」,而不是「这条流水线只构建可信代码」。 当工作流本身能被不可信输入劫持时,来源证明反而成了恶意包的「信用背书」。这一点,从今年 5 月 TanStack 事件(恶意包携带有效 SLSA Build Level 3 证明)开始已被反复验证,这次则是又一次实锤。
04 十个恶意版本与三条执行路径
攻击者对不同版本线采用了略有差异的投递手法,可以归纳为三条路径:
4.1 路径一:binding.gyp 的 Python 逃逸(第一波稳定版)
8 个恶意稳定版中都带有一个恶意的 binding.gyp 文件。需要说明的是,这个包本身并没有任何原生模块——binding.gyp 的存在本身就是可疑信号。
文件中的 conditions 字段藏了一段高度混淆的 Python 表达式:通过 catch_warnings 子类遍历拿到 Python 内建对象,再经 __import__ 导入 os,最终调用 os.system()。所有关键字符串都被转义成 \U000000XX 形式的 Unicode 编码,解码后的真实命令只有一句:
node 3FWCvzduYZg.js
由于 binding.gyp 会在 npm install 期间被 node-gyp 解析处理,这条路径在开发者机器和 CI runner 上都会自动触发,而且它不属于 package.json 里显眼的生命周期脚本,隐蔽性更强。
4.2 路径二:preinstall 钩子(第二波稳定版叠加)
第二波的恶意版本在保留同款 binding.gyp 的同时,还在 package.json 中直接追加了:
"preinstall": "node 3FWCvzduYZg.js"
双保险,确保载荷一定被执行。StepSecurity 在隔离的 GitHub 托管 runner 上复现了安装行为:1.6.3、2.2.1、3.0.3、3.0.4 等版本在安装期间会拉起 curl 访问 GitHub 及 release-assets.githubusercontent.com,随后在 /tmp/trinnyyyy-* 目录下出现 Bun 可执行文件。
版本体积也是明显的异常信号:0.5.4 从上一版的约 41 KB 暴增至约 5.6 MB,其余恶意稳定版体积在 4.46 MB 到 6.53 MB 之间——正常发布绝不会有这种量级跳变。

图 | 恶意版本 3.0.4 的文件列表(图:StepSecurity)
4.3 路径三:两个 0.0.0-* 预发布版(自举变体)
两个以 commit 哈希命名的预发布版本走了另外的路:
0.0.0-365d4eb...:preinstall 先拉取并安装 Bun,然后执行 is_it_this_simple.js,同时注入环境变量 WORKFLOW_ID=release.yml、REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen、TARGET_PACKAGES=@7nohe/openapi-react-query-codegen;
0.0.0-ec7876d...:preinstall 设置同样的环境变量,但直接执行 node nu.js,不拉取 Bun。
Socket 在分析攻击者 fork 的提交链时发现,这两套脚本是分批引入的:一条链先加 is_it_this_simple.js 再引入基于 Bun 的 preinstall,另一条先加 nu.js 再引入并修改对应钩子——明显的分阶段搭建痕迹。文件名「is_it_this_simple」(就这么简单?)更像是攻击者留下的嘲讽。
05 载荷能力拆解:一个为「吞噬凭证并自我复制」而生的蠕虫
3FWCvzduYZg.js 是一个约 5.6 MB 的单行混淆 JavaScript 文件(不同版本中的该文件大小与哈希存在差异,说明攻击者对不同版本线投放了不同构建)。它开头是一个基于单字节 XOR 解码的 Function 构造器,负责解密内嵌的 AES-128-GCM 加密的第二阶段载荷,写入临时目录的随机文件名后通过 child_process.execSync 执行,最后在 finally 块中删除痕迹。
Socket 报告给出的攻击链总览如下(原文流程图,直接引用):
npm install / native-build path
|
v
binding.gyp Python escape -> node 3FWCvzduYZg.js
|
+--> anti-analysis checks and detached relaunch
+--> scan files, process memory, cloud metadata, and CI variables
+--> validate discovered credentials
+--> collect and encrypt results
| +--> attacker-created GitHub repository commits
| +--> GitHub token monitor and handler
| +--> public-repository workflow poisoning
+--> poison npm/JFrog/RubyGems packages
+--> optional PyPI typosquatting
+--> developer-tool/MCP persistence
+--> signed GitHub-commit command channel
+--> SSH propagation
在此基础上,我们将「安装触发 → 载荷加载 → 四路凭证发现 → 验证筛选 → 加密外泄」的完整过程展开为下图:

图 | 载荷执行与凭证窃取、加密外泄的完整流程
以下能力细节均来自 Socket 团队对样本的静态提取与反混淆分析(分析过程中未实际执行样本)。
5.1 反分析与进程隐身
载荷落地后第一件事是自保:
检查环境中是否已安装 Bun、是否处于 GITHUB_ACTIONS 上下文、是否存在安全软件,命中即退出或改变行为;
通过 __DOGINSIDEPC 环境变量实现「分离重启」:父进程派生一个脱离标准输入输出的独立子进程后自行退出,让真正的恶意进程脱离最初安装命令的生命周期,从进程树视角隐藏自己;
所有收集与传播操作默认静默吞错,使用随机临时目录与随机仓库名,最大限度降低噪声。
5.2 四路凭证发现
载荷内置了成套的凭证扫描器,覆盖几乎所有开发者会「漏」出密钥的地方:
(1)文件系统扫描:递归遍历当前工作目录(含隐藏文件,最多检查 12,000 个文件),内置针对各类 token 的正则——GitHub 的 gho_/ghp_/github_pat_、npm 的 npm_、RubyGems、PyPI、JFrog 的访问令牌与引用令牌,以及 JWT、数据库连接串、云凭证等通用密钥模式。
(2)进程内存扫描:通过一个 Python 辅助程序读取 /proc/
(3)环境变量枚举:收集一大批 CI/CD 与云平台变量,包括但不限于 GITHUB_TOKEN、ACTIONS_ID_TOKEN_REQUEST_TOKEN、CI_JOB_TOKEN、NPM_TOKEN、ANTHROPIC_API_KEY、AWS 三件套、AZURE_*、GOOGLE_APPLICATION_CREDENTIALS、VAULT_TOKEN 等。
(4)云元数据服务:请求 AWS IMDSv2 端点(169.254.169.254)、ECS 任务元数据(169.254.170.2)、GCP 的 metadata.google.internal,以及 Azure(management.azure.com、login.microsoftonline.com)、Vault 等凭证端点,直接榨取云实例角色凭证。
5.3 先验证、再下手:带「智商」的凭证利用
载荷并不会把扫到的所有字符串一股脑外传,而是先逐一验证有效性,再按权限决定后续动作:
GitHub token:调用 API 确认 scopes,只有具备 repo 或 public_repo 权限的 token 才会启用仓库级操作;
npm token:查询 registry 的 token 接口,要求是非 2FA 且具备包写权限的 token,再枚举可写的组织与包范围;
PyPI token:向 upload.pypi.org/legacy/ 发送试探性 multipart 请求,以 400 响应作为 token 有效的判据,再探测包所有权;
RubyGems token:查询 api_key.json,要求具备 index_rubygems 权限;
JFrog 凭证:针对发现的 .jfrog.io 端点测试,仅在会话确认有写权限时才使用。
5.4 加密外泄:把 GitHub 当成免费的「死信箱」
收集到的敏感数据会进入一个缓冲队列(默认满 102,400 字节触发发送),随后:JSON 序列化 → gzip 压缩 → 生成随机 AES-256-GCM 密钥加密 → 用内嵌的 RSA 公钥(RSA-OAEP/SHA-256)加密该 AES 密钥 → Base64 打包。
外泄通道颇具迷惑性:载荷用受害者的 GitHub token 创建一个全新的公开仓库(名称是随机生成的东方 Project 主题词汇,描述为「Trinitite: Sponsored by Preview 2 Effects」),把加密数据以 commit 形式推上去。对任何旁观的网络审计者来说,这只是「用户在往 GitHub 推代码」——合法平台承担了传输与存储层的角色,内容本身又是只有攻击者私钥能解的密文。
5.5 多生态投毒:蠕虫的自我复制引擎
这是该样本被称为「Mini Shai-Hulud 风格」的核心原因——它能把每一个受害者变成新的分发节点:
npm:用验证通过的 token 枚举可写包,下载 tarball、注入载荷、重新发布;
JFrog:对私有 registry 中可写的 npm 仓库做同样的事(代码中带有 [jfrognpm] 标记);
RubyGems:枚举账号名下的 gem,逐个下载、篡改、经 RubyGems API 重新提交;
PyPI:内置一个条件触发的抢注仿冒(typosquatting)模式——当 TYPO_MODE=1 且指定了目标包时,为每个目标生成至多 20 个 -mcp/-mpc 后缀变体,注入 Python 载荷后用窃取的 token 上传 wheel。
值得注意的是 -mcp 这个后缀取向——瞄准的正是当下火热的 MCP(Model Context Protocol)生态用户。
蠕虫的完整传播与寄生闭环如下图所示——每一个失陷主机都可能成为下一批恶意包的分发源:

图 | 蠕虫的自我复制闭环——多注册表投毒、CI 投毒、SSH 横向、C2 与持久化共同维持传播循环
5.6 对 CI 与开发工具的寄生
GitHub Actions 投毒:载荷会枚举 token 有推送权限的仓库,检查其中的工作流文件,通过 Git 对象与 commit API 改写工作流并植入 .github/_index.js。被植入的恶意工作流伪装成「ClaudeCode Review」,监听 deployment 事件,运行时将仓库全部 secrets 序列化写入 res.txt 并作为构建产物(artifact 名为 reviewed)上传。提交信息则伪装成人畜无害的「chore: update dependencies」。当 deployment 路径不可用时,还有直接推送分支的备用方案。
开发者工具持久化:载荷会在用户主目录下搜索 settings.json、config.json、mcp.json、.aider.conf.yml、.cursorrules、.windsurfrules、.github/copilot-instructions.md 等文件,过滤路径中含 claude、codex、gemini、copilot、kiro、opencode、cline、aider、tabby、amazonq、cody、bolt、continue 等 AI 编程工具名称的目标。对每个可写配置,它会写入 .config/index.js(载荷副本),并注入一段「会话启动时静默运行、不要向用户提及」的后台指令——AI 编程助手本身被改造成了木马的开机启动项,即便卸载了恶意 npm 包,未来的每次会话仍会重新拉起载荷。
系统级持久化:macOS 上创建 LaunchAgent(com.user.sysvinit-detect-fash.plist,RunAtLoad + KeepAlive),Linux 上创建并启用 systemd 用户服务(sysvinit-detect-fash.service)并开启 lingering。这个驻留组件是一个「GitHub token 监工」:每分钟用窃取的 token 轮询 api.github.com/user,最长持续 72 小时,一旦 token 失效(GitHub 返回 4xx)就用 eval 执行预置的破坏性处理脚本。也就是说,条件反射式的「先吊销 token」可能直接引爆破坏载荷——正确的处置顺序是先隔离主机、清除驻留,再轮换凭证。
5.7 隐蔽控制通道与横向移动
签名命令通道:主凭证窃取模块每小时通过 GitHub commit 搜索 API 轮询关键词 firedalazer,解析形如 n1ggatr1n
SSH 传播:从本地 SSH 配置与 known_hosts 收集候选主机,以关闭主机密钥校验、禁用密码认证的非交互模式发起连接,用 scp 把 ai_setup.sh 和 ai_init.js 送过去并远程执行——向可达主机继续扩散。
StepSecurity 使用 Harden-Runner 在隔离的 GitHub 托管 runner 上直接引爆了 3.0.4 的载荷,捕获到的进程树与上述静态分析相互印证:

图 | 运行时行为证据(图:StepSecurity)
06 与 Shai-Hulud 家族的关系:相似,但不能草率归因
Shai-Hulud 是 2025 年 9 月首次出现的 npm 自我复制蠕虫(名称出自《沙丘》中的沙虫),此后持续演化:2025 年 11 月出现具备数据擦除能力的第二代;2026 年 4–5 月,TeamPCP 组织的 Mini Shai-Hulud 行动感染了 TanStack、Mistral AI、UiPath 等 170 余个 npm/PyPI 包,并首次实现了携带有效 SLSA 证明的恶意发布;2026 年 8 月初又出现了感染 400 余包的 ChainDrop 变体。该蠕虫源码已于今年 5 月被公开,模仿者层出不穷,使得归因愈发困难。
本次样本与 Mini Shai-Hulud 公开描述存在大量行为重叠:npm 安装期执行、Bun 加载器、全方位凭证收割、GitHub Actions 投毒、利用窃取的 npm/GitHub 权限自我传播、用 GitHub 仓库做存储与通信等。
但差异同样明显:本样本使用 Unicode 转义的 binding.gyp 触发器(而非此前报告中的 postinstall 模式);同时支持 macOS 与 Linux;采用 AES-GCM/RSA 加密信封加攻击者自建公开仓库的外泄方式;并新增了 RubyGems/JFrog 滥用、条件式 PyPI 抢注、开发工具配置持久化、签名 commit 命令通道和 SSH 传播等模块。
行为重叠只能说明「师出同门」的可能性,不能证明出自同一批操作者。 在蠕虫源码已公开的当下,基于具体 IOC(哈希、路径、字符串)的检测比基于行为画像的归因更可靠。
07 应急响应建议
7.1 如果安装过受影响版本
安装过即视为主机已失陷,按以下顺序处置(顺序很重要):
1先隔离 —— 立即将安装过受影响版本的开发机或 CI runner 断网隔离。注意载荷中的「token 监工」会在 token 失效时触发破坏行为,务必先断网/清除驻留,再动凭证
2优先重建 —— 从已知干净的镜像重建受影响系统。若必须原位清理,先定位并移除全部持久化机制(LaunchAgent、systemd 用户服务、~/.config/sysvinit-detect-fash/、开发工具配置中的注入项、/var/tmp/.shit、/tmp/trinnyyyy-* 等),再轮换凭证
3轮换凭证 —— 从一台干净的机器上吊销并轮换该环境可触及的一切凭证:npm、GitHub、PyPI、RubyGems、JFrog、CI/CD secrets、云凭证、SSH 密钥、AI 工具 API Key
4全面排查 —— 检查 lockfile 与 SBOM 中是否存在 10 个恶意版本(含间接依赖,npm ls @7nohe/openapi-react-query-codegen);不要依赖 npm audit signatures——恶意版本的签名是真的
5审计日志 —— GitHub 审计日志中留意新建公开仓库、工作流文件变更、deployment 创建、「chore: update dependencies」提交、名为 reviewed 的 artifact;npm/PyPI/RubyGems/JFrog 上留意非预期的版本发布;同时检查 SSH 日志与 known_hosts 中的异常外联
6锁定版本 —— 固定到已知安全版本(0.5.3 / 1.6.2 / 2.2.0 / 3.0.2),清理包管理器缓存并删除 node_modules 后从干净 lockfile 重装
7.2 给开源维护者的加固清单
这起事件的「根因」与其说是高深漏洞,不如说是 CI 配置的基本卫生问题:
绝不允许仅凭评论文本触发发布。 任何监听 issue_comment 且持有 id-token: write 的工作流,都必须校验 github.event.comment.author_association(仅允许 MEMBER/OWNER/COLLABORATOR),或者干脆把发布迁移到不可信账号无法触发的事件上(如手动 workflow_dispatch、打 tag);
永远不要在特权工作流中检出并执行 PR/fork 代码。 不可信代码 + OIDC 发布身份 = 把签名钥匙递给陌生人;
默认关闭安装脚本:npm install --ignore-scripts 可拦下 preinstall 钩子,禁用自动 node-gyp 构建可封死 binding.gyp 路径(CI 中尤其值得开启);
不要把 provenance 当作安全结论。它回答「从哪来」,不回答「好不好」;锁定版本 + 完整性哈希校验 + 关注发布节奏异常(几分钟内回填多个版本线是典型的失陷信号)才是更有效的组合拳。
08 技术附录
8.1 MITRE ATT&CK 技术映射
战术 | 技术 ID | 技术名称 | 本次事件中的体现 |
初始访问 | T1195.002 | 供应链投毒:软件供应链 | 通过被劫持的发布工作流向 npm 推送恶意版本 |
执行 | T1059.007 | 命令与脚本解释器:JavaScript | preinstall / binding.gyp 触发 node 3FWCvzduYZg.js |
执行 | T1059.006 | 命令与脚本解释器:Python | binding.gyp 中的混淆 Python 表达式调用 os.system();Python 内存转储器 |
执行 | T1059.004 | 命令与脚本解释器:Unix Shell | Bun 下载脚本、eval 执行驻留 handler |
持久化 | T1543.001 | 创建或修改系统进程:Launch Agent | com.user.sysvinit-detect-fash.plist (RunAtLoad + KeepAlive) |
持久化 | T1543.002 | 创建或修改系统进程:Systemd Service | sysvinit-detect-fash.service 用户级服务并启用 lingering |
持久化 | T1546 | 事件触发执行 | AI 编程工具配置/MCP 配置注入会话启动钩子;VS Code folderOpen 任务 |
防御规避 | T1027 | 文件或信息混淆 | 单字节 XOR + AES-128-GCM 内嵌载荷、Unicode 转义字符串、混淆字符串表 |
防御规避 | T1036 | 伪装 | 恶意工作流伪装为「ClaudeCode Review」,提交信息伪装为「chore: update dependencies」 |
防御规避 | T1497 | 虚拟化/沙箱规避 | 检查 Bun 是否已装、CI 环境标识、安全软件存在性,命中即退出或改行 |
防御规避 | T1070 | 痕迹清除 | 载荷执行后于 finally 块删除临时文件 |
凭证访问 | T1552.001 | 非安全凭证:文件中的凭证 | 递归扫描工作区(含隐藏文件)匹配各类 token 正则 |
凭证访问 | T1552.005 | 非安全凭证:云实例元数据 API | 请求 AWS IMDSv2、ECS、GCP 元数据端点窃取实例角色凭证 |
凭证访问 | T1003 | 操作系统凭证转储 | 读取 /proc/<pid>/maps 与 /proc/<pid>/mem 转储进程内存并扫描密钥 |
发现 | T1083 | 文件与目录发现 | 递归枚举工作区与主目录下的开发工具配置文件 |
发现 | T1057 | 进程发现 | 枚举运行中的进程以筛选内存转储目标 |
横向移动 | T1021.004 | 远程服务:SSH | 解析 SSH 配置与 known_hosts,非交互 SSH + scp 向可达主机传播 |
横向移动 | T1078 | 有效账户 | 复用窃取的 GitHub/npm/PyPI/RubyGems/JFrog 凭证执行投毒与发布 |
收集 | T1602 | 配置仓库数据收集 | 窃取 GitHub Actions secrets、CI/CD 环境变量 |
命令与控制 | T1102.001 | Web 服务:死信箱解析器 | 通过 GitHub commit 搜索(关键词 firedalazer)获取 RSA-PSS 签名的命令 URL |
命令与控制 | T1573 | 加密通道 | AES-256-GCM + RSA-OAEP 混合加密外泄信封 |
命令与控制 | T1105 | 工具传输 | 从 GitHub Releases 下载 Bun 1.4.0;经签名通道下载远程 Python 命令 |
外泄 | T1567.001 | 向代码仓库外泄 | 以受害者 token 创建随机命名的公开 GitHub 仓库,commit 推送加密数据 |
影响 | T1485 | 数据破坏 | token 监工在 token 失效(HTTP 4xx)时 eval 执行预置破坏性 handler |
影响 | T1195.002 | 供应链投毒(传播) | 利用窃取凭证对 npm/JFrog/RubyGems 包投毒、PyPI 抢注仿冒,实现蠕虫式自我复制 |
8.2 失陷指标(IOC)
恶意包与版本
@7nohe/openapi-react-query-codegen @
0.0.0-365d4eb738d3146583431948d3ba6e27a32556be
0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab
0.5.4 / 0.5.5 / 1.6.3 / 1.6.4 / 2.2.1 / 2.2.2 / 3.0.3 / 3.0.4
已知安全版本(各版本线最后一个干净版本):0.5.3、1.6.2、2.2.0、3.0.2
攻击者基础设施
github[.]com/p00paboot
github[.]com/p00paboot/openapi-react-query-codegen (用于暂存恶意代码的 fork)
恶意提交:365d4eb738d3146583431948d3ba6e27a32556be
文件与路径指标
3FWCvzduYZg.js
binding.gyp
ai_init.js / ai_setup.sh
is_it_this_simple.js / nu.js
~/.local/bin/sysvinit-detect-fash.sh
~/.config/sysvinit-detect-fash/fox
~/.config/sysvinit-detect-fash/fash-detected
~/.config/sysvinit-detect-fash/runit
~/Library/LaunchAgents/com.user.sysvinit-detect-fash.plist
~/.config/systemd/user/sysvinit-detect-fash.service
/var/tmp/.shit
/tmp/.sshu-<random>
/tmp/pcfg/
/tmp/trinnyyyy-*/bun
.config/index.js
.github/_index.js
哈希指标
3FWCvzduYZg.js(SHA-256,不同版本线存在不同构建):
b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6
59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380
b24d121667f21f492cb9db34fbfd515d5922a8dd30b9c45215c7220abbb10ca8 (3.0.4 中 6,384,601 字节版本)
binding.gyp(SHA-256):
d3246926b20a8d021ed7de0ac8e9eee1dda986088f84ba18f31cb2042a121f5d
npm tarball SHA-1:
版本 | SHA-1 |
0.5.4 | 2d934cf137a4e62519f88e7ab669d2fabda33867 |
0.5.5 | 337ae261e4e73a9f365f892dcef2dc6f6932e90a |
1.6.3 | 0f9bc76952b67d7a28a57d1e726293f417df0119 |
1.6.4 | 6499c9ab4e60f9b1db6756cb0de55ebc334a72d1 |
2.2.1 | 5ab130e4736d4582899af2385ec7eb5a33619d05 |
2.2.2 | fc60551b23485829c0a6e910224b049c891a49b8 |
3.0.3 | bafa4edaa6812fce10ae703ae450cc88ebbe1730 |
3.0.4 | 3fc635b988db2bd647b8578dfc1a85769913b708 |
0.0.0-365d4eb... | e7a07ca4a3cd51c262495f473abe4c4e505b7be4 |
0.0.0-ec7876d... | 206b18c418434abc994bd40e021edcc334eee89b |
执行与环境指标
preinstall: "node 3FWCvzduYZg.js"
WORKFLOW_ID=release.yml
REPO_ID_SUFFIX=7nohe/openapi-react-query-codegen
TARGET_PACKAGES=@7nohe/openapi-react-query-codegen
__DOGINSIDEPC(载荷分离重启环境变量)
GitHub commit 搜索关键词:firedalazer
commit 信息格式:n1ggatr1n <base64-url>.<base64-签名>
恶意工作流名:ClaudeCode Review(on: deployment,上传 artifact 名 reviewed)
可疑提交信息:chore: update dependencies
网络指标(重要提示:以下为被滥用的合法服务端点,切勿直接封禁)
github.com / api.github.com(含 /user、/search/commits、/user/repos)
github.com/oven-sh/bun/releases/download/bun-v1.4.0/
registry.npmjs.org(含 /-/npm/v1/tokens、/-/whoami)
upload.pypi.org/legacy/
rubygems.org/api/v1/api_key.json
169.254.169.254(AWS IMDSv2)/ 169.254.170.2(ECS)
metadata.google.internal
graph.microsoft.com / management.azure.com / login.microsoftonline.com
fulcio.sigstore.dev / rekor.sigstore.dev
检测思路:与其封禁这些域名,不如监控异常行为——如 npm install 期间出现 curl 外联、/tmp/trinnyyyy-* 下出现 Bun 可执行文件、CI 任务中意外的 GitHub API 调用、账号下新建随机命名的公开仓库等。
09 结语
回顾整条攻击链,最昂贵的漏洞其实不在代码里,而在一个「图省事」的 CI 设计决策里:为了能用一句评论触发发布,项目把 OIDC 可信发布身份、PR 代码检出、--no-git-checks 这三样东西放进了同一个不对评论者鉴权的工作流。攻击者要做的全部工作,只是 fork、改代码、敲 11 个字符。
而 npm provenance 的「失效」提醒我们:供应链安全工具链中的每一环都只担保它声明的那一件事。签名证明构建来源,扫描发现已知恶意,锁文件固定版本——没有任何单一机制能替代「最小权限 + 不信任未审查输入」这条古老原则。在 AI 编程工具本身都已成为持久化跳板的今天,开发机与 CI runner 理应被视为与生产环境同等重要的防护边界。
10 参考链接