加载失败
TanStack(一个流行的 React/TypeScript 开源工具集)的 npm 包被爆出供应链入侵,攻击者先借助 GitHub Actions(GitHub 的 CI/CD 服务)里的 `pull_request_target` 工作流和共享 cache 做 cache poisoning,再在发布流程中拿到 OIDC(OpenID Connect)短期凭证把恶意版本推上 npm。评论里提到,恶意 payload 还会在 Linux 上以 systemd user service、在 macOS 上以 LaunchAgent 的形式常驻,并在 token 被撤销后触发死手开关式的破坏动作。讨论因此分成两条主线:一条是 GitHub Actions 的默认权限、缓存和 fork 处理是否设计过于危险;另一条是 npm、pnpm、bun 等 JavaScript 包管理器默认执行 lifecycle scripts、依赖冷却期和锁文件策略是否应该更严格。TanStack 随后发布了 postmortem,并开始移除缓存、锁定 Actions 版本和收紧工作流。
评论普遍认为这次入侵的关键不是 npm 本身,而是 GitHub Actions 里的 `pull_request_target` 触发器和共享 cache 的组合。攻击者先在 fork 的 PR 环境里把 pnpm store 或相关文件投毒,再在后续受保护的 release/job 中被恢复,最终运行了恶意生命周期脚本并拿到发布凭证。有人指出文档其实已经警告过不要在 `pull_request_target` 里执行不可信代码,但这个 footgun 仍然太容易踩,连 `contents: read` 之类的配置也会让人误以为是只读。也有人补充说 GitHub 的 cache 默认跨受保护/非受保护边界共享,本身就是平台级设计问题。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10]
另一条主线是 Trusted Publishing(基于 OIDC 的短期发布凭证)到底能挡住多少攻击。支持者认为它至少消除了长期 npm token 的泄露面,也让攻击者必须先进入 CI 或拿到更高权限的 repo admin 身份。质疑者则强调,只要安全门槛放在 GitHub 这个同一信任域里,repo admin token 就可能直接改 workflow、关掉保护、改审批规则,等于从门里把锁拆了。很多人因此主张把最终发布再加一层 registry 侧的 2FA 或人工 promotion,而不是只依赖 GitHub 内部审批。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10] [来源11]
大量留言转向 package manager 的默认安全策略,尤其是 `prepare`/`postinstall` 这类 lifecycle scripts。很多人建议默认启用 `ignore-scripts`、设置 `minimum-release-age`/`min-release-age` 之类的冷却期,并尽量 `pin` 到锁文件和精确版本,避免刚发布就被装进来。有人指出 bun、pnpm、npm、Yarn、Deno、pip 现在都在补这类功能,但语法和默认值非常碎片化,甚至 `npm run` 与生命周期脚本的语义也让人容易误解。也有人提醒 bun 其实只对部分热门包默认允许脚本,并不等于彻底免疫。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10] [来源11] [来源12]
TanStack 发布的 postmortem 也被反复讨论,重点是他们准备怎么补洞。已公布的措施包括临时去掉 pnpm cache、清空 GitHub Actions caches、把 Actions 全部锁到 commit ID、强制非 SMS 的 GitHub 2FA、移除 CI 里的 `pull_request_target`,并计划加上 `zizmor` 这类 workflow lint 工具和 `.github` 目录的 `CODEOWNERS`。评论里有人认可这种快速整改,也有人觉得应进一步把 release cache 和 PR cache 完全隔离,甚至限制外部贡献者入口。
恶意 payload 的破坏性也让人印象深刻:它会安装监控脚本,Linux 上表现为 systemd user service,macOS 上则是 LaunchAgent,定时检查 GitHub token 是否被撤销。若检测到 40x,就触发 `rm -rf ~/.` 一类的破坏动作,所以不少人把它描述成“死手开关”或带威胁性的勒索式逻辑。围绕应对方式,很多人主张一旦确认感染就直接整机重装;也有人提醒保留受感染机器用于取证,或者至少依赖离线备份来降低损失。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10]
很多讨论最终回到操作系统层面的隔离问题:如果同一个用户能跑 npm、拿到 shell、甚至有 sudo 权限,那本地机器往往早就没法信任了。有人主张用独立用户、只给 `CAP_NET_BIND_SERVICE` 之类的 capabilities、rootless 服务、容器或更强的 VM 隔离;也有人直接提到 QubesOS(以多 VM 分区隔离为核心的系统)更接近正确方向。反方则指出 containers 不是安全边界,devcontainer 往往还会把 GitHub credentials 自动注入,Flatpak 也有 `/proc`、`/sys` 和 DBus 之类的泄漏面。整体共识是:只要把第三方代码放进有家目录访问权的环境里,攻击者迟早能把系统劫持。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10] [来源11] [来源12] [来源13]
还有一派把矛头指向 GitHub Actions 和它的默认体验,认为问题不是 YAML 还是 bash,而是平台把共享 cache、Secrets、审批和触发器缠在一起。有人提到 GitLab 会把 protected 和 unprotected cache 分开,而 GitHub 允许跨边界复用,导致一个 PR 就能污染后续主分支构建。评论也提到 `pull_request_target` 的名字本身就很误导,应该直接废掉,或者至少把 cache、network、secrets 全部改成显式 opt-in。另一部分人则转向 Nix、Dagger、Docker、Makefile 这类更可复现、可审计的构建方式,认为现有 CI 太依赖“你得先知道它哪里会坑你”。
[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10] [来源11]
pull_request_target: GitHub Actions 的 PR 触发器,默认在目标仓库上下文运行;如果再 checkout 不可信 PR,就容易变成高危入口。
cache poisoning: 把共享缓存写成恶意内容,让后续受信任流程恢复并执行它。
Trusted Publishing: npm/pypi 一类用 CI 里的短期身份凭证(OIDC)代替长期发布 token 的发布方式。
OIDC token: 由 OpenID Connect 颁发的短期身份令牌,常用于 CI 向包仓库证明身份。
minimum-release-age / dependency cooldown: 给新发布版本设冷却期,延迟一段时间再允许安装,用来争取发现和下架恶意包的时间。
lifecycle scripts: 包安装时自动执行的脚本,如 `prepare`、`postinstall`,常被用作供应链攻击入口。
SLSA provenance attestation: 构建来源证明,只能证明工件来自哪个 pipeline,不能证明这个 pipeline 没被污染。