GitHub Actions 滥用路径防御:Token 最小权限、触发器隔离与 Action 固定
1. 威胁模型与常见滥用分叉
GitHub Actions 滥用主要围绕“低信任事件进入高信任上下文”展开:
- 不可信事件上下文注入:
github.event.pull_request.title/body/head.ref或comment.body直接内联在run:Shell 中,导致任意命令执行与GITHUB_TOKEN/Secrets 外带。 pull_request_target与不安全检出组合:在 base 权限上下文中执行actions/checkout并指定ref: ${{ github.event.pull_request.head.sha }},使 fork PR 代码拿到读写 Token 与仓库 Secrets。- 默认宽权限与
self-hosted暴露:仓库未设置全局只读GITHUB_TOKEN,或公共仓库允许外部 PR 触发持久化self-hostedRunner,留下持久化后门或云元数据窃取面。 - 可变 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),云侧校验精确 sub | repo: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。
防御工作流
- 组织级基线设置:在 GitHub Organization/Repository Settings -> Actions -> General 中,将默认
GITHUB_TOKEN权限设为只读,并开启外部首次贡献者运行 Workflow 需人工审批(Require approval for all outside collaborators)。 - 存量 Workflow 扫描:运行检查脚本识别所有含
pull_request_target、内联${{ github.event.* }}、未固定 Commit SHA 的uses:以及缺失permissions:声明的文件。 - 触发器拆解与 OIDC 收敛:将特权操作(评论响应、打标签、发布制品)拆分为仅消费只读构建产物的
workflow_run或受保护environmentJob,并收紧云侧 OIDCsub条件。 - 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 |