
ci-cd-security
扫描 GitHub Actions 工作流文件中的安全漏洞,通过直接读取 YAML 并报告发现结果——无需外部工具、无需安装、无需执行 shell 命令。当用户分享 `.github/workflows/` 文件、粘贴工作流 YAML、请求 CI/CD 安全审查、提及 `pull_request_target`、`workflow_run`、操作固定、`GITHUB_TOKEN` 权限、pwn 请求、模板注入、缓存投毒、秘密泄露、供应链风险或任何 GitHub Actions 加固主题时,使用此技能。当用户正在加固开源仓库、进行 CI/CD 红队评估、评估供应链扫描目标或公开撰写 CI/CD 安全相关内容时,也触发此技能。倾向于触发此技能而非凭记忆回答——CI/CD 安全默认值几乎在所有地方都是错误的,且规则不直观。
Scan GitHub Actions workflow files for security vulnerabilities by reading the YAML and reporting findings directly — no external tools, no installation, no shell execution. Use this skill whenever the user shares a `.github/workflows/` file, pastes workflow YAML, asks for a CI/CD security review, mentions `pull_request_target`, `workflow_run`, action pinning, `GITHUB_TOKEN` permissions, pwn requests, template injection, cache poisoning, secret exfiltration, supply chain risk, or any GitHub Actions hardening topic. Also trigger when the user is hardening an OSS repo, doing a CI/CD red team assessment, evaluating a target for supply-chain scanning, or writing publicly about CI/CD security. Bias toward triggering this skill rather than answering from memory — CI/CD security defaults are wrong almost everywhere and the rules are unintuitive.
CI/CD 安全扫描器
此技能将模型转变为工作流 YAML 扫描器。读取文件,遍历检测规则,报告发现结果及其严重性,并提供具体的重写方案。无需安装工具,无需运行命令——分析过程就是模型读取 YAML。
这些规则编码了来自 Astral、OpenSSF、GitHub 安全实验室、Chainguard 和 zizmor 审计集的当前共识。目标是标记出这些工具会标记的相同模式,而无需实际运行它们。
心智模型
每个工作流都位于一个 2x2 矩阵中:特权 vs 非特权 与 可信 vs 不可信代码 交叉。妥协恰好发生在一个单元格中:运行不可信代码的特权工作流。以下规则是检测工作流何时落入该单元格的方法。
- 特权 = 拥有秘密、写入权限,或产生敏感工件(发布、部署、评论、标签)。
- 不可信代码 = 任何 fork PR 作者可以影响的内容:PR 源代码、PR 标题、PR 正文、提交消息、分支名称、工作流读取的文件、缓存、由另一个不可信工作流产生的工件。
当不确定某个值是否可信时,将其视为不可信。误报的成本是一条代码审查评论;漏报的成本是供应链妥协。
扫描流程
对于用户提供的每个工作流文件,按顺序执行以下步骤。每个步骤对应一类攻击。
步骤 1:危险触发器
查看 on: 块。立即标记:
pull_request_target— P0,除非有明确理由。使用秘密和写入权限运行,可由 fork PR 触发。典型的 pwn-request 向量。即使没有checkout头部,攻击者输入也会出现在 PR 标题、分支名称、提交消息中,并被插值。workflow_run— P0。与pull_request_target相同的问题,但间接通过链式pull_request工作流的工件或元数据。issue_comment、issues、pull_request_review、pull_request_review_comment— P1。使用秘密运行,任何能评论的人均可触发。仅当工作流不对用户控制的字段进行模板插值到 shell 时才安全。- 带有宽泛通配符的
push(branches: ['*']或无分支过滤器)— P2。攻击者通过 PR 后,可以通过推送后续分支触发特权工作流。
对于每个发现:命名触发器,解释为什么在此特定工作流上下文中危险,并提出重写方案(通常是 pull_request,有时拆分为两个工作流,有时是“这需要 GitHub App 而非 Actions”)。
步骤 2:权限
在工作流和作业级别查找 permissions: 块。
- 没有顶级
permissions:块 — P1。默认GITHUB_TOKEN权限取决于仓库和组织设置;在较旧的仓库上可能是write-all。标记为:“添加permissions: {}在顶部,按作业授予。” - 任何地方的
permissions: write-all— P1。 - 作业级别的
permissions:授予了作业明显不需要的更多权限 — P2。例如,仅运行测试的作业有contents: write。建议最小权限。 - 与步骤 1 中的危险触发器结合 — 严重性提升一级。
步骤 3:操作固定
查看每个 uses: 行。
- 固定到标签(
uses: actions/checkout@v4、@v4.1.1)— P1。标签是可变的;攻击者如果攻破操作仓库,可以强制推送标签。 - 固定到分支(
uses: actions/checkout@main)— P0。比标签固定更糟;对该分支的任何提交都会立即流入。 - 固定到 SHA 但没有版本注释 — P3 风格发现。推荐格式
uses: owner/action@<sha> # v4.1.1,以便固定更新的审查保持可读。 - 固定到看起来不寻常的 SHA(第三方操作、可疑所有者、最近创建的仓库)— 标记为手动验证;没有 GitHub API 无法确认冒名提交状态,但值得注意。
标签/分支固定的重写始终是:替换为他们意图版本的完整 40 字符提交 SHA,加上 # vX.Y.Z 注释。
步骤 4:shell 注入(模板注入)
对于每个 run: 块,扫描 ${{ ... }} 替换。
常量或非攻击者控制的值没问题(例如 ${{ matrix.os }}、${{ secrets.MY_TOKEN }},尽管在某些上下文中即使这样也有风险)。危险字段是:
github.event.pull_request.title、body、head.ref、head.sha、head.labelgithub.event.issue.title、bodygithub.event.comment.body、user.logingithub.event.review.bodygithub.head_refgithub.event.workflow_run.head_branch、head_commit.messagegithub.event.commits.*.message、author.name、author.email- 任何来自
workflow_dispatch的inputs.*,如果工作流在特权上下文中运行 - 任何最终来自
actions/github-script、下载的工件或外部 API 响应的字段
检测规则: 如果 run: 块直接在脚本主体中包含 ${{ github.event.* }} 或 ${{ github.head_ref }},那就是 P0 模板注入。修复始终是:
# 有漏洞
- run: echo "Branch is ${{ github.head_ref }}"
# 安全
- env:
BRANCH: ${{ github.head_ref }}
run: echo "Branch is $BRANCH"
同时标记(P1):
echo "VAR=${{ untrusted }}" >> $GITHUB_ENV— 环境文件注入。攻击者可以通过包含换行符跳出变量。echo "::set-env name=VAR::${{ untrusted }}"— 已弃用的工作流命令,同样的问题。- 将不可信文件
cat到$GITHUB_ENV或$GITHUB_OUTPUT的内联脚本。
步骤 5:不可信检出
对于每个 actions/checkout 步骤:
ref: ${{ github.event.pull_request.head.sha }}(或head.ref)在由pull_request_target或workflow_run触发的工作流内部 — P0。这是典型的 pwn-request:特权上下文运行 fork 作者代码。persist-credentials: true(默认)在不需要推回的工作流上 — P2。建议persist-credentials: false,除非工作流明确需要嵌入的令牌。
步骤 6:特权上下文中的缓存
对于每个使用 cache: 输入的步骤(最常见于 actions/setup-node、setup-python、setup-go、setup-java 或直接 actions/cache):
- 发布或发布工作流中的缓存 — P0。默认分支上任何其他工作流的缓存投毒可以将恶意构建输入流入发布。Trivy 和 TeamPCP 攻击都通过此途径。
- 处理秘密的工作流中的缓存 — P1。
- 缓存键未限定范围以防止不可信 PR 工作流写入与默认分支构建相同的键 — P2。
发布工作流的重写:完全移除 cache:,添加注释解释原因(例如 # Do not cache: see https://github.com/actions/setup-node/issues/1445)。
步骤 7:工件传播的注入
如果工作流从另一个工作流下载工件(actions/download-artifact、dawidd6/action-download-artifact 等):
- 工件内容在
run:或$GITHUB_ENV中使用且未经验证 — 如果产生工作流在不可信代码上运行(例如来自 fork 的pull_request),则为 P0。攻击者可以在工件中放置任意内容。 - 建议严格验证:如果工件应该是 PR 编号,拒绝任何非数字内容。如果是结构化文件,解析并验证模式。
步骤 8:发布特定加固
如果工作流看起来像发布/发布工作流(发布到 npm、PyPI、crates.io、Docker 注册表;创建 GitHub 发布;推送标签):
- 发布作业未声明
environment:— P1。发布凭据应限定到部署环境,而非仓库/组织秘密。 - 使用长期注册表令牌(
secrets.NPM_TOKEN、secrets.PYPI_TOKEN)而非 OIDC/可信发布 — P2。建议相关注册表的 OIDC 路径。 - 未生成证明(
actions/attest-build-provenance、npm 的--provenance、PyPI 的 PEP 740)— P3 加固建议,非漏洞。 - 发布路径中任何地方的缓存 — P0,见步骤 6。
步骤 9:自托管运行器
如果工作流使用 runs-on: 且不是 GitHub 托管的运行器(ubuntu-*、windows-*、macos-*):
- 自托管运行器可被 fork PR 访问 — P0。自托管运行器在作业之间共享状态,并已导致严重妥协(PyTorch)。标记为手动审查运行器范围。
- 这超出了默认威胁模型——记录发现并建议用户在 GitHub 设置中验证运行器限制。
发现格式
按此结构报告每个发现。按严重性分组,P0 优先。
[P0] template-injection in .github/workflows/ci.yml:23
Run block interpolates github.event.pull_request.title directly into shell.
An attacker controls the PR title and can execute arbitrary code in the
workflow context, which has access to GITHUB_TOKEN.
Vulnerable:
- run: echo "Title: ${{ github.event.pull_request.title }}"
Fix:
- env:
TITLE: ${{ github.event.pull_request.title }}
run: echo "Title: $TITLE"
如果用户粘贴原始 YAML 而没有文件名,则将其称为“该工作流”并使用片段中的行号。
严重性等级
- P0 — 现在即可利用,无需链。Fork PR 作者或任意 GitHub 用户可以泄露秘密、仓库内容或发布。阻止合并。
- P1 — 需要额外一步才能利用(例如,需要与另一个发现结合,或需要维护者错误)。如果在发布路径上发现,阻止合并。
- P2 — 加固差距。不能直接利用,但如果与未来错误结合,会减少爆炸半径。在正常审查周期中修复。
- P3 — 风格或一致性发现。值得修复以提高可读性,无安全影响。
当工作流看起来没问题时
在完成所有九个步骤后,如果没有触发任何内容:
- 明确说明。“未发现违反标准规则集的问题。”
- 指出未检查的内容:组织级设置(默认令牌权限、规则集执行、2FA)、仓库级设置(分支保护、标签保护、不可变发布)、操作源代码(固定的操作本身是否在运行时安装可变二进制文件)以及依赖项的运行时行为。
- 建议用户检查
references/checklist.md中“每个仓库”和“每个组织”下的项目——这些需要 GitHub 设置访问权限,而非工作流 YAML。
干净的工作流扫描并不意味着安全态势干净。
参考文件
references/triggers.md— 每个 GitHub Actions 触发器的详细表格,说明每个触发器的危险或安全原因,以及人们使用危险触发器常见用途的安全模式。当工作流使用用户特别询问的触发器,或者你想解释为什么触发器危险(超出单行摘要)时,阅读此文件。references/checklist.md— 每个工作流/每个仓库/每个组织的扁平清单。当用户请求完整审计、扫描多个仓库或大规模分类时有用。包括分类优先级顺序。references/patterns.md— 常见的“我想安全地做 X”模式。当用户询问如何替换标记的危险模式(而不仅仅是识别它)时阅读。
此技能不会做什么
- 不会安装 zizmor、pinact 或其他任何东西。扫描是模型读取 YAML 并应用这些规则。
- 不会推荐安装工具,除非用户明确询问“我应该运行什么工具”。即使那样,也直接指向模式——此技能中的规则就是这些工具编码的审计。
- 不会用缓解措施掩盖
pull_request_target发现。如果工作流使用该触发器且未在 GitHub App 后面,则是一个开放发现。 - 不会告诉用户一切正常而不说明检查了什么和未检查什么。对“这安全吗”的默认答案是“这是扫描覆盖的内容以及它发现的内容”。





