mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3245 字
9 分钟
供应链投毒与CI/CD安全:从TanStack事件看云安全信任链
2025-01-28

2026年5月11日19:20 UTC,npm registry开始接收一批异常的包发布——42个@tanstack/*包,84个恶意版本,在6分钟内集中上线。20分钟后,外部安全研究员ashishkurmi在GitHub issue中贴出了完整的技术分析。

这不是一起简单的npm token泄露事件。攻击者没有窃取任何npm凭据,没有入侵维护者的机器,甚至没有触碰发布工作流本身。他们用了三把钥匙,每把单独都打不开门,但三把组合在一起,门就开了。

TanStack事件是2026年供应链攻击精密化趋势的缩影。本文将从前置知识出发,建立供应链安全的认知框架,然后深入复盘TanStack事件的技术细节,最后构建CI/CD管道的防御体系。

一、前置知识:软件供应链与信任模型#

1.1 什么是软件供应链#

软件供应链(Software Supply Chain)是指从源代码到最终运行产物的完整路径——每一个环节都可能被攻击者利用:

graph LR A["源代码"] --> B["依赖包<br/>npm/pip/maven"] B --> C["CI/CD管道<br/>GitHub Actions"] C --> D["构建产物<br/>Docker镜像/tarball"] D --> E["发布仓库<br/>npm registry/ACR"] E --> F["部署环境<br/>K8s/VM"] style A fill:#87CEEB style B fill:#FFD700 style C fill:#FFA500 style D fill:#FF6B6B style E fill:#FF6B6B style F fill:#DDA0DD

传统安全关注的是运行时(F)的防护——WAF、EDR、微隔离。供应链安全关注的是到达运行时之前的路径(A→E)是否可信。如果构建产物在到达部署环境之前就已经被植入后门,运行时的所有防护都将失效。

1.2 npm生态的信任模型#

npm生态的信任模型建立在几个隐含假设上:

信任假设含义攻击面
包名即身份@tanstack/react-query就是官方包Typosquatting:注册@tanstack/react-qurey
维护者可信有发布权限的人不会发布恶意代码账号接管:钓鱼/凭据窃取
registry可信npmjs.org上的包就是源码对应的包中间人/registry劫持
CI/CD可信GitHub Actions产出的包就是预期产物Cache投毒/Action投毒/OIDC提取

供应链攻击的本质,就是打破这些隐含假设。

1.3 GitHub Actions CI/CD基础#

理解TanStack事件需要了解GitHub Actions的几个关键概念:

  • pull_request触发器:在Fork的PR中运行,使用Fork的代码和Fork的CI配置,没有Base仓库的写权限——安全
  • pull_request_target触发器:在Fork的PR中运行,但使用Base仓库的CI配置和密钥——危险,因为Fork的代码在Base仓库的信任上下文中执行
  • GITHUB_TOKEN:临时token,受permissions:约束,PR触发的workflow默认只有read权限
  • OIDC token:通过id-token: write权限获取的临时身份令牌,用于npm trusted publishing
  • actions/cache:CI缓存,scope为per-repository,post-job save不受permissions:约束
graph TB subgraph "pull_request(安全)" A1["Fork代码"] --> B1["Fork CI配置"] B1 --> C1["无写权限<br/>沙箱执行"] end subgraph "pull_request_target(危险)" A2["Fork代码"] --> B2["Base CI配置"] B2 --> C2["Base密钥/权限<br/>信任上下文"] end style C1 fill:#90EE90 style C2 fill:#FF6B6B

1.4 当前态势:供应链攻击演进#

供应链攻击在过去三年经历了从”包级别”到”CI/CD级别”再到”生态级别”的演进:

graph TB subgraph "2021-2023: 包级别" A1["Typosquatting<br/>crossenv冒充cross-env"] A2["依赖混淆<br/>Alex Birsan研究"] A3["维护者账号接管<br/>ua-parser-js事件"] end subgraph "2024-2025: CI/CD级别" B1["Action投毒<br/>tj-actions/changed-files"] B2["Cache投毒<br/>Adnan Khan文档化"] B3["OIDC令牌提取<br/>Runner内存读取"] end subgraph "2026: 生态级别" C1["三链攻击<br/>TanStack事件"] C2["自传播<br/>跨维护者污染"] C3["伪造AI身份<br/>利用信任心理"] end

关键趋势:攻击者不再满足于单个漏洞的利用,而是将多个已知漏洞串联成链式攻击,每个漏洞单独都不足以完成攻击,但组合后可以跨越多道信任边界。

二、TanStack事件复盘:三链攻击#

2.1 攻击全景#

graph LR A["pull_request_target<br/>Pwn Request"] --> B["Cache投毒<br/>跨信任边界"] B --> C["OIDC令牌提取<br/>Runner内存读取"] C --> D["npm恶意发布<br/>84个版本"] style A fill:#FF6B6B style B fill:#FFA500 style C fill:#FFD700 style D fill:#ff6b6b

攻击者将三个已知漏洞串联,每个单独都不足以完成攻击,但三者组合跨越了三道信任边界:

漏洞跨越的信任边界单独是否可利用
pull_request_target Pwn RequestFork代码 → Base仓库CI否(仅能执行代码)
GitHub Actions Cache投毒PR的CI → Main的发布CI否(需要先有代码执行)
OIDC令牌内存提取发布CI运行时 → npm registry写权限否(需要先在Runner上执行代码)

2.2 第一链:Pwn Request#

bundle-size.yml使用了pull_request_target触发器,对Fork的PR自动运行,并在该上下文中checkout了Fork的合并代码并执行构建:

on:
pull_request_target:
paths: ['packages/**', 'benchmarks/**']
jobs:
benchmark-pr:
steps:
- uses: actions/checkout@v6.0.2
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- uses: TanStack/config/.github/setup@main
- run: pnpm nx run @benchmarks/bundle-size:build

攻击者创建了一个Fork(zblgg/configuration,故意重命名以规避Fork列表搜索),提交了包含3万行捆绑JS载荷的恶意commit,然后以[skip ci]前缀压制CI,再通过PR将恶意代码引入pull_request_target的执行上下文。

关键问题pull_request_target绕过了首次贡献者审批门控。PR的pull_request触发的工作流被正确阻止了,但pull_request_target的工作流直接放行。

2.3 第二链:Cache投毒#

恶意代码vite_setup.mjs被精心设计,向pnpm store目录写入数据,使用的key恰好匹配正式release.yml工作流会查找的key:

Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}

benchmark-pr任务结束,actions/cache@v5的post-step将已被投毒的pnpm store保存到该key下。当release.yml下次在main分支上运行时,其Setup Tools步骤恢复了被投毒的缓存条目。

这是GitHub Actions的已知设计问题,由Adnan Khan在2024年5月公开文档化。核心矛盾:

  • actions/cache@v5的post-job save不受permissions:约束——Cache写入使用Runner内部token,不是GITHUB_TOKEN
  • 设置permissions: contents: read不会阻止Cache写入
  • Cache scope是per-repo的,pull_request_target运行和main上的push共享同一Cache scope

2.4 第三链:OIDC令牌内存提取#

release.yml声明了id-token: write(合法的npm OIDC可信发布所需)。当被投毒的pnpm store恢复到Runner后,攻击者控制的二进制文件被调用:

# 攻击流程(简化)
# 1. 定位GitHub Actions Runner.Worker进程
runner_pid = find_process("/proc/*/cmdline", "Runner.Worker")
# 2. 读取进程内存映射
maps = read_file(f"/proc/{runner_pid}/maps")
mem = read_file(f"/proc/{runner_pid}/mem")
# 3. 从内存中提取OIDC令牌
oidc_token = extract_oidc_token(maps, mem)
# 4. 直接POST到npm registry
requests.post("https://registry.npmjs.org/-/npm/v1/tarball",
headers={"Authorization": f"Bearer {oidc_token}"})

这绕过了工作流中定义的”Publish Packages”步骤——恶意代码在测试/清理阶段就铸造了OIDC令牌并直接发布。

这是与2025年3月tj-actions/changed-files妥协事件完全相同的内存提取技术,攻击者甚至使用了原文的Python脚本(保留了归属注释)。

2.5 恶意载荷行为#

当开发者或CI环境对受影响版本执行npm install时:

graph TB A["npm install"] --> B["解析optionalDependencies"] B --> C["下载orphan payload commit"] C --> D["执行prepare生命周期脚本"] D --> E["运行router_init.js<br/>~2.3MB混淆代码"] E --> F["采集凭据"] E --> G["外泄数据"] E --> H["自传播"] F --> F1["AWS IMDS/Secrets Manager"] F --> F2["GCP metadata"] F --> F3["K8s service-account tokens"] F --> F4["Vault tokens"] F --> F5["~/.npmrc"] F --> F6["GitHub tokens"] F --> F7["SSH私钥"] G --> G1["Session/Oxen Messenger<br/>filev2.getsession.org"] H --> H1["枚举受害者维护的其他包"] H --> H2["重新发布注入版本"]

外泄通道值得注意:使用Session/Oxen Messenger的文件上传网络(filev2.getsession.org),端到端加密,没有攻击者控制的C2——因此只能通过IP/域名阻断,无法通过内容检测拦截。

自传播机制:通过registry.npmjs.org/-/v1/search?text=maintainer:<user>枚举受害者维护的其他包,然后重新发布注入版本。这意味着一个受感染的CI环境可以污染整个npm生态中该维护者的所有包。

2.6 时间线#

时间 (UTC)事件
5月10日 17:16攻击者创建Fork zblgg/configuration
5月10日 23:29恶意commit 65bf499d写入Fork,作者伪造为claude <claude@users.noreply.github.com>
5月11日 10:49PR #7378开启,pull_request_target工作流自动运行
5月11日 11:11Force-push将恶意commit推到PR head,bundle-size.yml执行恶意代码
5月11日 11:29被投毒的Cache条目(1.1GB)保存到refs/heads/main scope
5月11日 11:31攻击者force-push回当前main HEAD,PR变为0文件no-op,关闭并删除分支。Cache投毒持续存在
5月11日 19:15合法PR合并触发release.yml,恢复被投毒的Cache
5月11日 19:20npm registry接收首批恶意发布(42包,~42版本)
5月11日 19:26npm registry接收第二批恶意发布(~42版本)
5月11日 ~19:50外部研究员ashishkurmi (StepSecurity) 开issue报告,完整技术分析

三、CI/CD攻击面深度分析#

3.1 六大攻击手法对比#

攻击手法原理典型案例检测难度防御复杂度
Typosquatting注册与流行包名相似的恶意包crossenv冒充cross-env
依赖混淆利用npm解析优先级植入内部同名包2021年Alex Birsan研究
维护者账号接管钓鱼/凭据窃取获取npm发布权限2022年ua-parser-js
Action投毒入侵热门GitHub Action注入恶意代码2025年tj-actions/changed-files
Cache投毒跨信任边界污染CI缓存2026年TanStack事件极高极高
OIDC内存提取从Runner进程内存读取临时令牌2025-2026年多次复现极高极高

3.2 为什么Cache投毒特别危险#

GitHub Actions的Cache设计存在一个根本性的信任边界问题:

  • Cache scope是per-repository的,不是per-workflow或per-trigger的
  • pull_request_target运行使用base仓库的Cache scope
  • actions/cache的post-job save不受permissions:约束
  • 这意味着Fork PR的CI可以向Main分支发布CI使用的Cache写入任意数据

这不是TanStack特有的问题——任何使用pull_request_target + actions/cache的仓库都面临相同风险

3.3 信任链视角#

从信任链的角度看,供应链攻击的本质是信任的传递与滥用

graph LR A["Fork代码<br/>❌ 不可信"] -->|"pull_request_target<br/>信任传递"| B["Base CI执行<br/>✅ 可信上下文"] B -->|"Cache save<br/>信任传递"| C["Cache条目<br/>✅ 可信数据"] C -->|"Cache restore<br/>信任传递"| D["发布Runner<br/>✅ 可信环境"] D -->|"OIDC铸造<br/>信任传递"| E["npm registry<br/>✅ 可信发布"] style A fill:#FF6B6B style B fill:#FFA500 style C fill:#FFA500 style D fill:#FFA500 style E fill:#FF6B6B

每一步都是信任的传递,每一步都假设上一步是干净的。攻击者的策略是找到信任链中最弱的一环,然后沿着链向下渗透。

四、防御体系#

4.1 GitHub Actions加固#

# GitHub Actions安全加固清单
github_actions_hardening:
pull_request_target:
- 规则: 绝不在pull_request_target中checkout Fork代码
- 规则: 绝不在pull_request_target中运行Fork控制的构建命令
- 规则: 如必须使用,仅用于只读操作(标签、评论)
- 替代: 使用pull_request触发 + 首次贡献者审批
cache:
- 规则: 不在pull_request_target工作流中使用actions/cache
- 规则: 如必须使用,设置cache-key前缀隔离PR和Main的Cache
- 规则: 定期审查和清除Cache条目
- 替代: 使用actions/upload-artifact + download-artifact(scope为workflow run)
oidc:
- 规则: 限制id-token: write仅用于实际发布步骤
- 规则: 将发布步骤与测试步骤分离为独立job
- 规则: 考虑添加provenance-source-verification检测异常发布路径
- 替代: 短期classic token + 手动审批
actions:
- 规则: 所有第三方Action引用固定到SHA而非tag
- 规则: 定期审计所有使用的第三方Action
- 规则: 使用StepSecurity的harden-runner Action限制网络和文件系统访问

4.2 npm生态防护#

npm_ecosystem_defense:
安装时:
- 使用npm audit持续扫描已知漏洞
- 部署Socket.dev或Phylum等供应链安全工具
- 在CI中添加package-lock.json完整性校验
- 监控optionalDependencies中的异常github:引用
发布时:
- 使用npm OIDC trusted publishing替代长期token
- 配置npm provenance签名
- 限制scope的maintainer列表(减少凭据暴露面)
- 发布后自动验证tarball内容与预期一致
响应时:
- 建立npm publish实时监控和告警
- 准备应急deprecation脚本
- 与npm安全团队建立快速沟通渠道
- 受影响后立即轮换所有可达凭据

4.3 凭据轮换清单#

任何在2026-05-11安装了受影响TanStack版本的主机,应视为可能被入侵,需轮换:

credential_rotation:
云平台:
- AWS: 所有AccessKey + IMDS可达的Secrets Manager值
- GCP: 所有Service Account Key + metadata可达的凭据
- Kubernetes: 所有ServiceAccount Token
- Vault: 所有Token和动态凭据
开发平台:
- GitHub: Personal Access Token + SSH Key
- npm: 所有发布token
- Git: .git-credentials中存储的所有凭据
基础设施:
- SSH: ~/.ssh/下所有私钥
- CI/CD: 所有CI环境变量中的密钥

4.4 “信任中断”原则#

纵深防御的本质不是在每一层都加锁,而是在信任链中插入不信任的断点

graph LR A["Fork代码"] -->|"❌ 不信任"| B["CI执行"] B -->|"❌ Cache校验"| C["Cache恢复"] C -->|"❌ 发布路径验证"| D["OIDC铸造"] D -->|"❌ tarball校验"| E["npm发布"] style A fill:#FF6B6B style B fill:#FFD700 style C fill:#FFD700 style D fill:#FFD700 style E fill:#90EE90
信任断点实现方式
Cache恢复后校验内容hash不盲目信任缓存数据
OIDC令牌铸造时验证调用来源检测是否从预期workflow step铸造
npm install后验证tarball比对provenance签名与预期内容
CI运行时监控/proc/*/mem访问检测异常的进程内存读取行为
发布后验证产物完整性自动比对发布产物与构建日志

五、我的思考#

5.1 供应链安全是云安全的阿喀琉斯之踵#

云安全的所有技术——零信任、微隔离、EDR、CNAPP——都建立在一个隐含假设上:你部署的代码是你以为的代码。当这个假设被打破,所有上层防护都是空中楼阁。TanStack事件表明,攻击者不需要突破你的云安全体系,只需要在你信任的包里植入一行代码。

5.2 “信任传递”是核心问题#

从Fork代码到Base仓库Cache,从Cache到发布Runner,从Runner到npm registry——每一步都是信任的传递,每一步都假设上一步是干净的。攻击者的策略永远是找到信任链中最弱的一环,然后沿着链向下渗透。

5.3 防御者需要”信任中断”#

纵深防御的本质不是在每一层都加锁,而是在信任链中插入不信任的断点——Cache恢复后校验hash、OIDC铸造时验证来源、npm install后验证签名。每一个断点都是对”上一步是干净的”这个假设的挑战。

5.4 开源生态的系统性风险#

TanStack事件暴露了npm生态的几个结构性问题:

  • npm的unpublish政策:如果包有依赖者就不能unpublish,导致恶意tarball只能等待npm安全团队服务端移除,增加了数小时的暴露窗口
  • OIDC trusted publishing的盲区:一旦配置,workflow中任何代码路径都可以铸造发布令牌,没有per-publish review
  • 维护者凭据的爆炸半径:7个maintainer的npm scope意味着7个独立的凭据窃取目标,但影响范围相同

这些问题不是TanStack能独自解决的,需要npm/GitHub生态层面的系统性改进。

六、总结#

TanStack npm供应链投毒事件是2026年云安全领域的一个标志性事件——不是因为它的规模(42个包、84个版本),而是因为它的精密程度。攻击者没有使用任何novel技术,而是将三个已知漏洞串联成一个完整的攻击链,每个漏洞单独都不足以完成攻击。

这揭示了一个更深层的问题:云安全的威胁正在从”单点突破”演变为”链式攻击”。防御者需要从”加固每一层”转向”中断信任链”。

2026年的供应链安全需要三个转变:

  1. 从静态扫描到行为监控——不仅检查包内容,还监控CI运行时行为(异常内存访问、异常网络请求)
  2. 从信任传递到信任中断——在每个信任边界插入验证断点
  3. 从单点防御到纵深防御——GitHub Actions加固 + npm生态防护 + 运行时检测,缺一不可

供应链安全不是可选的加固项,而是云安全的基石。当基石松动,上层建筑再精密也无法立足。


事件来源:Postmortem: TanStack npm supply-chain compromise — Tanner Linsley, 2026-05-11. GitHub Security Advisory: GHSA-g7cv-rxg3-hmpx.


参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

供应链投毒与CI/CD安全:从TanStack事件看云安全信任链
https://blog.souloss.com/posts/cloud-security/supply-chain-poisoning-cicd-security/
作者
Souloss
发布于
2025-01-28
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时