Skip to content

GitHub Actions 滥用路径防御:Token 最小权限、触发器隔离与 Action 固定 ​

1. 威胁模型与常见滥用分叉 ​

GitHub Actions 滥用主要围绕“低信任事件进入高信任上下文”展开:

  1. 不可信事件上下文注入:github.event.pull_request.title/body/head.ref 或 comment.body 直接内联在 run: Shell 中,导致任意命令执行与 GITHUB_TOKEN/Secrets 外带。
  2. pull_request_target 与不安全检出组合:在 base 权限上下文中执行 actions/checkout 并指定 ref: ${{ github.event.pull_request.head.sha }},使 fork PR 代码拿到读写 Token 与仓库 Secrets。
  3. 默认宽权限与 self-hosted 暴露:仓库未设置全局只读 GITHUB_TOKEN,或公共仓库允许外部 PR 触发持久化 self-hosted Runner,留下持久化后门或云元数据窃取面。
  4. 可变 Tag 引用与宽松 OIDC:第三方 Action 使用 @v3 等可变标签,或云端 OIDC 信任策略匹配 repo:ORG/REPO:*,使任意分支均可换取高权云凭证。

2. 核心加固策略与控制矩阵 ​

风险分叉加固措施配置要求防御效果
上下文命令注入全部通过 env: 传入并用双引号引用禁止 run: 中出现 ${{ github.event.* }}阻断引号闭合与 $(...) 命令替换
pull_request_target 误用优先用 pull_request + workflow_run 两段式拆解;禁止在 pull_request_target 检出 PR head 执行代码非必要不使用 pull_request_target彻底隔离 fork 代码与写权限/Secrets
GITHUB_TOKEN 权限过大组织与仓库设置默认 Read repository contents,Workflow 顶层声明 permissions: contents: read仅特定发布 Job 单独提升必要权限即使某步失陷也无法篡改分支或 Release
第三方 Action 供应链全部第三方 Action 固定 40 位完整 Commit SHA,并启用 Dependabot 跟踪更新uses: actions/checkout@<40-hex-sha>防止上游 Tag 被劫持导致全流水线投毒
self-hosted Runner公开仓库禁止使用 self-hosted;私有仓库开启 Require approval for all outside collaborators 且采用一次性 Runner禁用持久化工作区与元数据访问消除跨 Job 污染与驻留后门
OIDC 与 Environment 门禁部署 Job 绑定 environment: production(含 Required Reviewers),云侧校验精确 subrepo:ORG/REPO:environment:production阻止非受保护分支换取生产 STS Token

加固后的安全 Workflow 模板 ​

yaml
name: Secure PR Check
on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false

      - name: Safely validate PR metadata
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}
          PR_BRANCH: ${{ github.head_ref }}
        run: |
          printf 'Title: %s\nBranch: %s\n' "$PR_TITLE" "$PR_BRANCH"

注意在 actions/checkout 中显式设置 persist-credentials: false,避免构建进程后续意外读取 .git/config 中留存的授权 Token。

防御工作流 ​

  1. 组织级基线设置:在 GitHub Organization/Repository Settings -> Actions -> General 中,将默认 GITHUB_TOKEN 权限设为只读,并开启外部首次贡献者运行 Workflow 需人工审批(Require approval for all outside collaborators)。
  2. 存量 Workflow 扫描:运行检查脚本识别所有含 pull_request_target、内联 ${{ github.event.* }}、未固定 Commit SHA 的 uses: 以及缺失 permissions: 声明的文件。
  3. 触发器拆解与 OIDC 收敛:将特权操作(评论响应、打标签、发布制品)拆分为仅消费只读构建产物的 workflow_run 或受保护 environment Job,并收紧云侧 OIDC sub 条件。
  4. CI 门禁固化:通过 CODEOWNERS 保护 .github/workflows/ 目录,要求任何流水线变更必须由安全或平台团队 Review 后才可合入。

3. Python GitHub Actions 安全基线校验脚本 ​

python
#!/usr/bin/env python3
"""GitHub Actions Workflow 安全配置基线检查工具(SHA 固定、Token 权限、注入与触发器)。"""

import re

USES_RE = re.compile(r"uses:\s*([^\s#]+)")
SHA_PIN_RE = re.compile(r"^[^@]+@[0-9a-f]{40}$")
UNSAFE_CTX_RE = re.compile(r"run\s*:[^\n]*\$\{\{\s*github\.(?:event\.|head_ref)")


def validate_gha_workflow(content: str) -> list[str]:
    issues: list[str] = []
    if "permissions:" not in content or "permissions: write-all" in content:
        issues.append("missing explicit least-privilege permissions block")
    if "pull_request_target" in content and "github.event.pull_request.head" in content:
        issues.append("critical: pull_request_target combined with PR head checkout/ref")
    if "self-hosted" in content:
        issues.append("warning: self-hosted runner requires ephemeral isolation verification")
    if UNSAFE_CTX_RE.search(content):
        issues.append("high: untrusted github context interpolated directly in run step")

    for ref in USES_RE.findall(content):
        if ref.startswith("./"):
            continue
        if not SHA_PIN_RE.match(ref):
            issues.append(f"medium: action not pinned to 40-char commit SHA ({ref})")
    return issues


if __name__ == "__main__":
    risky_yml = """
on: pull_request_target
permissions: write-all
jobs:
  pr:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: echo "${{ github.event.pull_request.title }}"
"""
    hardened_yml = """
on: pull_request
permissions:
  contents: read
jobs:
  pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
        with:
          persist-credentials: false
      - env:
          TITLE: ${{ github.event.pull_request.title }}
        run: printf '%s\\n' "$TITLE"
"""
    risky_issues = validate_gha_workflow(risky_yml)
    clean_issues = validate_gha_workflow(hardened_yml)
    print("risky:", risky_issues)
    print("hardened:", clean_issues)
    assert len(risky_issues) >= 5
    assert len(clean_issues) == 0

验证与判定标准 ​

检查项判定为已加固(PASS)判定为存在风险(FAIL)验证方式
Token 默认权限仓库/Workflow 显式设定最小权限且 checkout 带 persist-credentials: false隐式宽权限或 permissions: write-all核查仓库 Settings 与 YAML permissions 块
触发器与上下文无 pull_request_target 检出 PR head;无 ${{ github.event.* }} 直接入 run:存在未中转的 PR/Comment 插值或高危 pull_request_target运行 validate_gha_workflow 零 HIGH/CRITICAL
Action 版本固定外部 Action 全部使用 40 位 Commit SHA 固定使用 @main、@v3、@v4 等可变标签validate_gha_workflow 检查零 SHA 告警
OIDC 与审批门禁生产部署绑定 environment 审批与精确 sub,.github/workflows 受 CODEOWNERS 保护任意分支或 PR 可直接请求云端 Role Token核查 AWS/GCP Trust Policy 与分支保护规则

MCP 工具映射 ​

防御步骤MCP 工具说明
检索 Actions 防护知识kb_router按 GitHub Actions / pull_request_target / OIDC 检索
对照攻击利用面kb_read_file读取 ctf-website/10-cloud/github-actions-abuse-paths 核对路径
关联流水线与供应链防御kb_catalog查看 ci-cd-pipeline-defense 与 dependency-confusion-defense

Apache-2.0 · For authorized learning and defensive research only · Disclaimer