GitHub Copilot Autofix自动修复删除关键安全配置,被恶意利用入侵Snowflake,文章详解GitHub Actions加固步骤。
On June 23,一个自主 AI 安全工具走进了 Snowflake 的内部 Jira。它没有暴力破解登录,也没有找到泄露的密码。门是五天前由一个"由 Copilot Autofix 驱动 AI 协同创作"的提交打开的,而唯一需要的钥匙是:用一条精心编写的标题打开 GitHub Issue 的能力。
Wiz Research 于 8 月 17 日发布了完整报告(Red Agent Exploits Snowflake Vuln Created by Copilot Autofix)。这是我见过的关于新型失败模式最清晰的案例:AI 编码助手悄悄删除了保障流水线安全的 security pattern,而自主代理在同一周内就利用了结果。
我每周都通过 GitHub Actions 部署 Spring Boot 应用,也有自己的 agent 基础设施。读到这份报告后,我检查了自己接触的每一个 workflow 文件,并改了四处。这篇文章就是这段经历:Snowflake 究竟哪里出了问题,为什么 AI 生成的变更是根本原因,以及你可以今晚就复制到自己流水线中的确切加固步骤。
目标是 snowflakedb/snowflake-connector-net,一个公开仓库。其 jira_issue.yml workflow 在 issues: opened 上运行,意味着任何 GitHub 用户只需在公开仓库上提交一个 Issue 就能触发它。
2026 年 6 月 18 日。一条由 Copilot Autofix 协同创作的提交(4a1b8ce,PR #1218)重写了 workflow 的部分内容。AI 删除了仓库原有的安全模式——即将 Issue 标题通过 env: 变量传递并用 jq 构建 JSON payload。它将其替换为在 shell 脚本中直接对不可信标题进行字符串展开。
2026 年 6 月 23 日。Wiz 的 Red Agent,一个通过 Snowflake 的 HackerOne 计划工作的自主 AI 安全研究 agent,发现了脚本注入漏洞,加以利用,从 runner 中拉取了凭证。Wiz 当天即披露。
2026 年 6 月 23 日。Snowflake 在数小时内修补了 workflow(提交 1dc7766,PR #1402),恢复了安全模式,并轮换了受影响的凭证。
2026 年 7 月 25 日。按照 Snowflake 的披露政策公开披露。
被窃取的 token 以 qa@snowflake.net 身份认证,并授予了对 Snowflake 工程、安全合规和 bug 悬赏跟踪项目的跨项目读取权限。一个零成本的 PoC 演变成了对一家大公司内部工具的广泛读取访问。
以下是 AI 修改前仓库已有的模式。标题作为环境变量传递,jq 构建 JSON payload:
- env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
jq -n --arg title "$ISSUE_TITLE" ...
以下是 Copilot Autofix 替换后的内容:
- run: |
TITLE=$(echo '${{ github.event.issue.title }}' | sed 's/"/\\"/g' | sed "s/'/\\'/g")
这看起来像是 AI 尝试对输入做转义。这是陷阱所在。GitHub 在 shell 看到脚本之前就展开了 ${{ github.event.issue.title }},所以 sed 转义运行得太晚了。标题中的一个单引号落入 shell 源码内部,破坏了 echo '...',剩下的内容作为命令执行。转义是在已经展开的文本上执行的,这意味着它永远不会有效。
GitHub 自己的文档(contexts)明确说明了安全模式所依赖的事实:工作流表达式在命令运行前求值,因此任何来自事件 payload 的内容接触到 run: 块时必须经过 env: 变量,绝不能内联插值。
workflow 有一个 if: 条件,看起来具有保护性:
if: (github.event_name == 'issues' && github.event.pull_request.user.login != 'whitesource-for-github-com[bot]')
在 issues 事件上,github.event.pull_request 始终为 null。所以条件简化为 null != 'whitesource-for-github-com[bot]',这永远为真。每个 GitHub 用户都通过了该门,包括攻击者。
这是第二个教训:guard 条件必须根据触发器对应的实际事件 schema 进行检查。一个因为字段不存在而静默求值为 true 的条件,比没有条件更糟糕,因为它看起来像是保护。
很容易将其解读为"Copilot 生成了一个 bug"。这没有抓住重点。人类开发者也会犯同样的错误,但 AI 版本有三个结构性上更糟糕的特性:
AI 完成了一次安全回归,而非 bug。删除的模式并非偶然的。将不可信输入通过 env: 传递并用 jq 解析是一种明确的防注入设计。AI 不知道该模式存在的原因,所以把安全控制当作了代码风格清理。
AI 建议被合并得更快。Autofix 到达时已经由生成它的工具预先批准了。审查负担被倒置了:人类必须主动在一个看起来像常规重构的变更中抓出一个微妙的注入 bug。
攻击者也是 AI。Red Agent 在第一次窃取尝试时遇到了 bash 语法错误,分析了错误,调整了 payload 正确闭合 shell 块,重新尝试并成功。发现窗口不再以攻击者技能衡量,而是以工具能力衡量。
Wiz 自己的核心结论很直白:"自动化 AI 助手通常缺乏关于特定代码模式为何被选中的历史上下文。"修复方案不是停止使用 Copilot。修复方案是假设 AI 创作的变更可能删除安全控制,并构建能捕捉到它的门。
我的编辑器里有 Copilot,PR 流程中有 Autofix 风格的建议,但我没有自己运行 Wiz 的 Red Agent。以下是我读完报告后应用到自己的 Spring Boot 流水线的加固措施,以及我针对 GitHub 文档验证过的检查。你的情况会因技术栈不同而有差异,但这些规则与 provider 无关。
Rule 1: Never interpolate event payloads into run: blocks.
在 workflow 中搜索 run: 内的 ${{ github.event.* }}。将每个出现位置移到 env: 块中。这是单次最高价值的变更,因为它一次性消灭了整类注入:
- name: Build payload
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
run: |
jq -n --arg title "$ISSUE_TITLE" --arg author "$ISSUE_AUTHOR" \
'{title: $title, author: $author}' > payload.json
shell 只看到环境变量,而 jq --arg 保证 JSON 结构完整,无论输入内容是什么。
Rule 2: Audit every if: gate against the trigger's event schema.
对于每个 workflow,写下触发它的事件,并检查 gate 中引用的每个字段是否与该事件的 schema 对应。如果一个字段可能为 null,条件并没有在做它看起来在做的事。GitHub 有一个公开的 webhook 事件和 payload 列表。对于 issues,没有 pull_request 对象,就是没有。
Rule 3: Treat AI-authored PRs as untrusted code.
在我现在合并 agent 生成变更的仓库中,我要求对每个触及的 workflow 文件都进行新的手动批准,并在合并前对 CI 文件本身运行静态分析:
actionlint 检查 workflow 语法并捕获虚假的表达式。它在许多情况下会标记在 issue 触发器上使用 github.event.pull_request,在一条命令中运行。
zizmor 专门扫描 workflow 的注入和安全反模式,包括对 run: 的模板注入。它就是为这类失败场景设计的。
CodeQL 在其支持的语言(包括 Java)的应用代码中捕获 sanitizer 回归。
一个最小化的合并检查如下:
- name: Lint workflows
run: |
actionlint -color
zizmor .
如果任一工具发现问题,分支就不会合并。这是"AI PR 必须经过与人类代码相同的静态分析和安全审查"的实践版本。
Rule 4: Give the runner only what it needs, for as long as it needs it.
Snowflake 被泄露的 token 是一个长期凭证,对 Atlassian 内部有广泛的读取访问权限。两种缓解措施缩小了任何未来泄露的爆炸半径:
在云提供商支持的地方使用 OpenID Connect(OIDC)代替存储的密钥。GitHub 的安全加固指南有设置步骤。runner 获得每个 job 铸造的短期 token,因此没有什么可被窃取的。
将每个存储密钥的权限范围限定到最小权限和最小资源集。一个可以读取所有项目的 Jira 集成 token 即使在攻击者发现它之前就是一个设计缺陷。
Rule 5: Make the guardrails explicit in code review, not in vibes.
最便宜的控制是在 PR 模板中添加一个检查清单,适用于任何触及 .github/workflows/ 的变更:
以下是我保存在自己仓库旁边的版本:
这些工具在 Snowflake bug 发生之后都不存在。有效的 guardrail 是在合并之前、对每个变更运行的,无论是否 AI 创作。
标题是"AI 发现了一个漏洞"。标题之下是:AI 引入了它,AI 发现了它,AI 在同一周内利用了它。我们现在进入了一个循环,同一项技术产生并消费着 bug,而中间的人类流水线是唯一还没有被自动化的部分。这是个奇怪的地方,值得对每一个触及你构建系统的特别生成的 diff 保持怀疑的眼光。
我每周都写关于 Java、Spring Boot 和 AI 的文章。订阅,免费。
你审计过自己的 GitHub Actions workflow 中的模板注入吗?你发现了什么意料之外的东西?