潜进TeamPCP核心群的几个月:开源供应链的真实攻击面
一个安全分析员在黑客团伙的核心群里待了几个月,看着对方一步步把开源包变成投毒工具,最后把关键身份线索交给执法机构。这个故事听起来像谍战,但对维护者来说,真正值钱的部分不是卧底本身,而是事发时那套 CI 工作流到底哪个环节被绕过了。这也是本文的证据边界所在:卧底部分只能指回 Wired 的一篇采访,而 CI 工作流那部分有当事方自己的复盘可以逐条核对。
结论先行
- 这次攻击的技术含量并不高,靠的是三处工程疏忽的叠加。TanStack 官方复盘写得很直白:
pull_request_target触发模式、跨信任边界的 GitHub Actions 缓存投毒、从 runner 进程内存里抠出 OIDC token,三者缺一不可,而且每一处都有公开的缓解办法(当事方一手复盘,来源见文末)。 - 外部公告不是第一道防线。TanStack 是自己被人从外面发现的——StepSecurity 的研究员 ashishkurmi 在首个恶意版本发布约 26 分钟后提交了完整分析(官方复盘「Response timing」表中「Compromise publicly detected」一行)。也就是说,从投毒到你收到告警,中间那段空白期只能靠你自己的发布约束来兜。
- 真正把损失压下来的动作是「吊销+轮换」,不是「通知」。Google 的做法是先找 AWS、微软批量吊销泄露凭证,再给被影响企业发信,因为按名单逐个通知根本赶不上(一手采访,见 Wired)。
- 攻击者拿到 50 万条凭证,却只勒索到「几万美元量级」。也就是说,凭证的价值并不自动等于现金——但对你而言风险并没有变小,因为凭证可以被转卖给别的团伙,而那时候你收到的就不是勒索信了(该金额为 Google 研究员的估算,不是官方统计)。
- 依赖安装脚本的信任级别,等于你在那台机器上直接跑了一段陌生代码。TanStack 事件里,恶意逻辑就是挂在一条
optionalDependencies上——"@tanstack/setup": "github:tanstack/router#79ac49ee…"——再由 npm/pnpm 的prepare生命周期脚本拉起一个约 2.3 MB 的混淆文件(官方复盘「IOC fingerprints」)。
证据与过程:三处疏忽怎么串成一条链
先看公开的事实边界。澳大利亚联邦警察(AFP)在 2026 年 8 月 27 日刊出的通报里给的表述是:「估计恶意代码可能影响了全球 1000 家以上机构,导致 50 万条以上凭证被窃取,外泄至少 300 GB 数据」;同一篇通报说「迄今为止,财务影响包括据估计达数亿美元的全球修复成本」;两名西澳男子(21 岁、23 岁)合计被控 14 项罪名。分清这里的三个口径,比记住数字更要紧:
| 那句话 | 一句话口径 | 该怎么标 |
|---|---|---|
| 1000 家以上机构、50 万条以上凭证、至少 300 GB | AFP 通报的措辞是「估计(It is estimated)……可能(potentially)」 | 警方在指控阶段的估算,不是审计结论 |
| 修复成本「数亿美元」 | 通报原句是「据估计达数亿美元的全球修复成本」 | 同样是估算;参照同类口径,本文一律标为「估算」,不写「官方数据」 |
| 14 项罪名、年龄与所在州 | 直接来自通报正文 | 可直接引为事实 |
两名被告在庭前均未被定罪,案件仍在程序中——这一点在转述时不该省略。
谷歌这边只有一条来源:Wired 记者 Andy Greenberg 在 2026 年 9 月 18 日刊出的采访。谷歌威胁情报组(GTIG)研究员 Austin Larsen 在 SentinelOne 的 LABScon 会议上披露,Mandiant 有一名卧底分析员(身份未公开,Larsen 明确说不是他自己)在 TeamPCP 曝光初期就进入了核心圈,3 月被拉进那个约 12 人的核心聊天室 CanisterWorm。Larsen 的措辞是这名分析员只做「墙上的苍蝇」,没有参与或怂恿任何入侵。这条线是媒体一手采访,不是谷歌官方报告:我没有找到 Google 或 Mandiant 自己发布的相关报告或声明,所以可信度标为「一手采访、单一信源」。
(顺带说清「卧底时长」这个数字的来源边界:我最初写标题时用了「卧底两年」,但这句话在原始来源里找不到依据。Wired 的原话是该 persona 花了「好几个月」建立信任,3 月进入核心群,Larsen 的表述是「几乎第一天(almost day one)」就已经在场,整个行动跨度以月计。所以标题与正文一律不采用「两年」,它属于未能核实的数字。顺带补一条 Wired 里同样具体、但常被转述省略的细节:这次内圈收缩是 TeamPCP 在发现 ShinyHunters 私吞勒索款之后自己做的——把数据搬到新服务器,并把 ShinyHunters 和包括谷歌卧底在内的几名成员一起踢出 CanisterWorm 聊天室。也就是说,卧底的暴露原因不是被识破,而是被同一批清理动作顺带清出去的。)
再看维护者真正该看的那部分——TanStack 的官方复盘把整条链拆得很清楚:
| 时间(UTC,2026-05-11) | 事件 | 来源标注 |
|---|---|---|
| 19:20:39 | npm 收到 42 个包的约一半恶意版本发布,走的是 OIDC 可信发布,但不是工作流里那个「发布」步骤发的 | 官方复盘 |
| 19:26:14 | 第二个 run 发出每包第二个版本,合计 84 个恶意版本 | 官方复盘 |
| 19:46 | StepSecurity 研究员 ashishkurmi 提 issue #7383 并附完整指纹 | 官方复盘 |
| 21:03 | 全量 42 包 / 84 版本完成弃用 | 官方复盘 |
| 22:13–23:55 | npm 侧陆续移除全部受影响 tarball,第一个 22:13:38,最后一个 23:55:26 | 官方复盘 |
官方的响应耗时统计是这样的:从首个恶意版本发布到被公开发现约 26 分钟(TL;DR 里的说法是「20 到 26 分钟,取决于该版本属于两批发布中的哪一批」),从发现到全量弃用约 1 小时 17 分钟。注意这两段的含义完全不同——前一段你无法控制,后一段才是维护者能练的能力。
技术链路上,攻击者的顺序是:
1. fork 一个仓库(改名以躲开 fork 列表检索),在分支上提交恶意 payload
2. 用 pull_request_target 工作流 checkout 该 PR 的 merge ref 并执行构建
→ 分支代码在「基线仓库」的权限与缓存作用域里跑起来了
3. 脚本把污染数据写进 pnpm store,并以 release.yml 之后会查找的缓存 key 保存
(缓存写入用的是 runner 内部令牌,permissions: contents: read 挡不住)
4. 下一次 push 到 main 触发 release.yml,恢复被污染的缓存
5. 恶意二进制在 runner 上执行,读 /proc/<pid>/mem 抠出 OIDC token
6. 用该 token 直接 POST 到 registry.npmjs.org 发布 —— 绕过了工作流里真正的发布步骤
第 5 步的 id-token: write 本身没有错:npm 官方文档明确推荐用 OIDC 可信发布来替代长期 access token(官方文档,npm Docs「Generating provenance statements」)。问题在于,一旦「不可信代码能在 runner 上执行」,这个短期凭证就变成了攻击者的发布通道。GitHub 官方文档的原话更狠一点:「不必要时避免使用 pull_request_target 触发器」,并且这类工作流不得显式 checkout 不可信代码;同一份文档还写明「把 action 钉到完整长度 commit SHA 是目前唯一能以不可变发布方式使用 action 的办法」(官方文档,GitHub Docs「Secure use reference」)。另外要注意,Actions 本身对 fork PR 有首次贡献者审批闸门,而 pull_request_target 恰好绕过它——TanStack 复盘里就记着:那次 PR 的 pr.yml(用 pull_request)一直卡在审批没跑,pull_request_target 的两个工作流却自动跑了(官方复盘)。
一个维护者能今天就做的清单
按「对应本次事故的哪一环」来排,不做泛泛的安全口号。
pull_request_target只做评论、打标签这类不执行代码的事;任何需要 checkout 并构建 fork 代码的 job,改用pull_request,并保留首次贡献者审批闸门——本次事故里正是pull_request那个工作流卡在审批上没跑。- 把第三方 action 引用钉到 commit SHA,不要用
@main或浮动 tag。TanStack 的加固 PR 做的就是这个。 - 缓存 key 区分信任级别:PR 触发的工作流不要与 main/发布工作流共用同一缓存命名空间,也别把
pnpm store这类可执行产物跨边界复用。 id-token: write只给真正需要发布的那一个 job,不要在工作流顶层声明;测试、构建、清理阶段都不给。- 依赖安装阶段默认不信脚本:CI 里用
npm ci --ignore-scripts,并把包管理器的「最小发布年龄」约束打开——pnpm 的minimumReleaseAge就是干这个的(官方文档,pnpm「Settings」)。OpenAI 在官方事件说明里的说法是,他们当时正在推进这批控制项,而两台受影响设备尚未更新配置,配置本可以挡住恶意包的下载(官方说明,OpenAI 官方博客)。 - 告警订阅要覆盖「恶意包」而不仅是 CVE:本次受害包在 OSV 这类恶意包数据库里有独立条目和 IOC 域名——
git-tanstack.com、filev2.getsession.org、api.masscan.cloud、seed1.getsession.org,这四条都能在条目里逐字查到(官方数据库条目,OSV MAL-2026-3489)。 - 把轮换写成剧本。官方建议:只要在 2026-05-11 装过受影响版本,就把该主机上可达的 AWS、GCP、Kubernetes、Vault、GitHub、npm、SSH 凭证全部轮换,且要在「另一台机器上」操作。
- 发布凭据与代码签名证书要能一键换。OpenAI 的处置是轮换代码签名证书,官方 FAQ 里点明的受影响平台是 Windows、macOS、iOS、Android,并要求 macOS 用户在期限内更新应用(官方说明,OpenAI 官方博客)。这一步慢一天,下游用户就多一天风险窗口。
把上面几条压成一份可以贴在 on-call 文档里的最小剧本:
# 收到「依赖包被投毒」告警后的头 30 分钟
# 1) 冻结新增安装面
npm ci --ignore-scripts
# 2) 受影响版本是否真进过锁文件(以本次事件的指纹位为例)
grep -n "optionalDependencies" -A3 package-lock.json | grep -i "tanstack/setup"
# 3) 网络侧处置:OSV 条目登记的 IOC 域名包括 git-tanstack.com、
# filev2.getsession.org、api.masscan.cloud、seed1.getsession.org
rg -n "git-tanstack.com|filev2.getsession.org|api.masscan.cloud" /var/log/ /etc/
# 4) 装过受影响版本 → 在另一台干净机器上轮换该主机可达的全部凭证
# (AWS / GCP / Kubernetes / Vault / GitHub / npm / SSH,官方给出的清单)
# 5) 反查发布通道是否被滥用
npm view @your-scope/your-pkg versions --json | tail -20
第 3 步有个细节值得记住:官方复盘指出恶意回传走的是端到端加密的 messenger 文件上传网络,不存在攻击者可控的 C2,所以网络侧唯一的缓解手段就是封域名或 IP(官方复盘)。复盘正文点名的回传域名是 filev2.getsession.org 和 seed{1,2,3}.getsession.org;上一条里的四个域名取自 OSV 条目的 IOC 字段,两者可以互相印证,但别把 OSV 那四条当成复盘原文的照抄。
一个可以自己算的规模指标:AFP 说全球修复成本估算「数亿美元」、影响 1000 家以上机构。取 1 亿到 5 亿美元作保守区间,除以 1000 家,单家机构的外部可见修复成本大致落在 10 万到 50 万美元(这是我的推算,用来估量级)。必须说清这个算式的性质:AFP 没有给出任何单家数字,也没有说明「数亿」的取整方式,把总量除以机构数本身就不是 AFP 的口径,真实分布大概率高度不均(少数大机构承担大部分成本)。这个量级和「一台开发机被投毒」听起来完全不匹配,原因正是第 5 条:一台机器上的凭证可以通向整个组织的云环境。
自己的判断
这篇素材里最容易写歪的地方,是把它写成一个「正义黑客卧底」的爽文。但把时间线拉平看,卧底的价值集中在三件事上:提前知道攻击目标、发现凭证囤积服务器、以及从对手的操作失误里拼出身份线索。这三件事都是「情报」,不是「防御」。而且必须承认:这三件事全部来自 Wired 对 Larsen 的一次专访,谷歌自己没有发布对应报告,我核对不到第二个独立信源——所以它们是「有出处的叙述」,不是「被交叉验证的事实」。真正让下游少流血的,是 Google 拿着情报去 AWS、微软批量吊销凭证这个动作——它把「逐个通知」换成了「按可利用面封堵」。这对中小团队同样成立:你没有情报源,但你可以先把凭证的可利用面收窄。
第二点判断是关于「官方 provenance 能不能救你」。npm 文档自己写清楚了:有 provenance 不代表包内没有恶意代码,它只证明「这个包是从哪、按哪份构建说明构建的」(官方文档原句:it does not guarantee the package has no malicious code)。TanStack 事件里,恶意版本恰恰是通过合规的 OIDC 可信发布通道发出去的——复盘的原话是发布「经 OIDC 可信发布绑定认证,但并非来自工作流定义的发布步骤」。所以 provenance 是溯源工具,不是检测工具;把它当检测工具用,会漏掉这一类攻击。
第三点,我认为社区最该抄的不是工具,而是「响应时间表」这个习惯。TanStack 把每个里程碑的耗时都算出来了(从发布到公开发现约 26 分钟、从发现到全量弃用约 1 小时 17 分钟、到 npm 侧最后一个包被移除约 4 小时 35 分钟)。有这张表,你才知道自己的瓶颈在发现还是在处置;没有这张表,事故复盘只剩情绪。
核实方法与残余不确定性
这篇稿子是被审稿意见打回来重修的,所以把核实方式一并交底,也方便读者判断哪些句子可以照抄、哪些只能当线索:
| 结论 | 核实方式 | 可信度 |
|---|---|---|
| AFP 的 1000 家、50 万条、300 GB、数亿美元、14 项罪名 | 打开 AFP 官网通报正文,逐句比对原措辞 | 唯一来源,但属官方一手;「数亿」「可能」是估算口径 |
| TanStack 时间线、三处根因、响应耗时 | 打开 TanStack 复盘全文,读时间线表与「Response timing」表 | 当事方一手,逐条可对 |
OpenAI 两台设备、证书轮换、minimumReleaseAge |
打开 OpenAI 官方说明(含 FAQ) | 当事方一手 |
git-tanstack.com 等四个 IOC 域名 |
打开 OSV MAL-2026-3489 条目,读 database_specific.iocs 字段 |
官方数据库字段,可逐字查 |
| 「provenance 不等于安全」 | 打开 npm Docs 原文,引其原句 | 官方文档 |
「不用 pull_request_target 的语境」 |
打开 GitHub Docs「Secure use reference」原文 | 官方文档 |
| 「卧底数月、约 12 人核心群、几万美元勒索」 | Wired 单一采访,抓全文核对原句 | 二手转述;无第二信源,且从未有 Google 官方文本 |
| 「修复成本数亿美元」的具体构成 | 找不到分摊口径 | 未能核实,见上文 |
这次没核实的
- 「卧底两年」:未能核实,已从标题和正文删除。原始来源(Wired)只支持「好几个月 / 几乎第一天 / 3 月进入核心群」这类以月计的量级。
- 地下团伙的勒索总收入:只有 Google 研究员 Larsen 的估算「几万美元量级」,非官方统计,也没有审计口径。
- 谷歌卧底行动本身:只有 Wired 记者 Greenberg 对 Larsen 的一次采访(2026-09-18),未找到 Google/Mandiant 的第一手报告或声明,也没有第二个独立信源;卧底分析员的身份、在场时长、具体接触对象均未公开,本文不作补全。
- Google 披露的 AI 辅助零日漏洞与本案的直接关联:Wired 采访称谷歌 5 月发布过一份案例研究,未点名 TeamPCP;我未找到该案例研究原文,因此本文不把它当作已核实事实使用。
- 确切的受害者完整名单:Wired 与 AFP 均只列举了部分(如 GitHub、Mercor、OpenAI、欧盟委员会),公开报道对大量受害方匿名处理,完整名单无法核实。
- Shai-Hulud(2025 年 9 月)与 TeamPCP 的关系:Wired 明确写了「仍不清楚是否有关联」,本文不做推断。
- 「数亿美元修复成本」的构成:AFP 只给了总量估算,没有分摊方法、没有统计机构,也没有说明是否包含凭证轮换、客户赔偿等科目;本文的单家推算仅用于量级参照。
参考来源
- AFP 媒体通报:两名西澳男子被控(2026-08-27)(官方一手)
- Wired:An Undercover Google Analyst Infiltrated a Notorious Supply-Chain Hacking Gang(Andy Greenberg,2026-09-18)(媒体一手采访)
- TanStack 官方复盘:Postmortem: TanStack npm supply-chain compromise(当事方一手)
- TanStack 官方加固说明:Hardening TanStack After the npm Compromise(当事方一手)
- OpenAI 官方事件说明:对 TanStack npm 供应链攻击事件的回应(2026-05-13)(当事方一手)
- pnpm 官方文档:Settings(
minimumReleaseAge等供应链相关配置项)(官方文档) - OSV 恶意包条目 MAL-2026-3489(@tanstack/start-plugin-core)(官方数据库)
- GitHub Docs:Secure use reference(Actions 安全加固)(官方文档)
- npm Docs:Generating provenance statements(官方文档)
- Security Affairs:Australian Police Charge Two Over TeamPCP Credential Theft(2026-08-27)(二手报道)
- ThaiCERT:OpenAI Affected by Supply Chain Attack Involving Malicious TanStack Packages(2026-05-18)(二手转述)
评论区
登录后可评论。