ci-cd-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CI/CD Security Scanner

CI/CD安全扫描器

This skill turns the model into a workflow-YAML scanner. Read the file, walk the detection rules, report findings with severity and a concrete rewrite. No tools to install, no commands to run — the analysis is the model reading the YAML.
The rules encode the current consensus from Astral, OpenSSF, GitHub Security Lab, Chainguard, and the zizmor audit set. The goal is to flag the same patterns those tools would flag, without needing to run them.
此技能可将模型转换为工作流YAML扫描器。读取文件、执行检测规则、并附带严重性等级和具体重写方案报告检测结果。无需安装工具,无需运行命令——分析过程仅需模型读取YAML即可。
检测规则整合了Astral、OpenSSF、GitHub Security Lab、Chainguard以及zizmor审计集的当前共识。目标是标记出这些工具会识别的相同风险模式,无需实际运行工具。

Mental model

思维模型

Every workflow sits on a 2x2: privileged vs unprivileged crossed with trusted vs untrusted code. Compromise happens at exactly one cell: privileged workflow running untrusted code. The rules below are ways to detect when a workflow ends up in that cell.
  • Privileged = has secrets, write permissions, or produces a sensitive artifact (release, deploy, comment, label).
  • Untrusted code = anything a fork PR author can influence: PR source code, PR title, PR body, commit messages, branch names, files the workflow reads, caches, artifacts produced by another untrusted workflow.
When unsure whether a value is trusted, treat it as untrusted. The cost of a false positive is a code review comment; the cost of a false negative is a supply chain compromise.
每个工作流都处于一个2×2矩阵中:特权 vs 非特权 交叉 可信代码 vs 不可信代码。攻击恰好发生在一个象限:运行不可信代码的特权工作流。以下规则用于检测工作流是否落入该象限。
  • 特权 = 拥有密钥、写入权限,或生成敏感产物(发布、部署、评论、标签)。
  • 不可信代码 = 任何分支PR作者可以影响的内容:PR源代码、PR标题、PR正文、提交信息、分支名称、工作流读取的文件、缓存、其他不可信工作流生成的产物。
当不确定某个值是否可信时,将其视为不可信。误报的代价只是一条代码审查评论;漏报的代价则是供应链攻击。

Scan procedure

扫描流程

For each workflow file the user provides, walk these passes in order. Each pass corresponds to a class of attack.
对于用户提供的每个工作流文件,按顺序执行以下检查步骤。每个步骤对应一类攻击场景。

Pass 1: dangerous triggers

步骤1:危险触发器

Look at the
on:
block. Flag immediately:
  • pull_request_target
    — P0 unless explicitly justified. Runs with secrets and write permissions, triggerable by fork PRs. The canonical pwn-request vector. Even without
    checkout
    of head, attacker input shows up in PR title, branch name, commit messages, and gets interpolated.
  • workflow_run
    — P0. Same problem as
    pull_request_target
    but indirect, via a chained
    pull_request
    workflow's artifacts or metadata.
  • issue_comment
    ,
    issues
    ,
    pull_request_review
    ,
    pull_request_review_comment
    — P1. Run with secrets, reachable by anyone who can comment. Safe only if the workflow does no template interpolation of user-controlled fields into shell.
  • push
    with broad wildcards
    (
    branches: ['*']
    or no branch filter) — P2. An attacker who lands a PR can fire a privileged workflow by pushing a follow-up branch.
For each finding: name the trigger, explain why it's dangerous in this specific workflow's context, and propose the rewrite (usually
pull_request
, sometimes a split into two workflows, sometimes "this needs a GitHub App not Actions").
查看
on:
块,立即标记以下内容:
  • pull_request_target
    — 除非有明确合理的理由,否则为P0级风险。该触发器会携带密钥和写入权限运行,可被分支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”)。

Pass 2: permissions

步骤2:权限配置

Look for
permissions:
blocks at workflow and job level.
  • No top-level
    permissions:
    block
    — P1. The default
    GITHUB_TOKEN
    permissions depend on repo and org settings; can be
    write-all
    on older repos. Flag with: "add
    permissions: {}
    at top, grant per-job."
  • permissions: write-all
    anywhere — P1.
  • Job-level
    permissions:
    granting more than the job clearly needs
    — P2. e.g.
    contents: write
    on a job that only runs tests. Recommend least privilege.
  • Combined with a dangerous trigger from Pass 1 — escalate severity by one level.
查看工作流和作业级别的
permissions:
块。
  • 无顶层
    permissions:
    — P1级风险。默认的
    GITHUB_TOKEN
    权限取决于仓库和组织设置;在旧仓库中可能为
    write-all
    。标记提示:“在顶层添加
    permissions: {}
    ,并为每个作业按需授权。”
  • 任何位置出现
    permissions: write-all
    — P1级风险。
  • 作业级
    permissions:
    授予的权限超出作业实际需求
    — P2级风险。例如,仅运行测试的作业设置了
    contents: write
    。建议遵循最小权限原则。
  • 与步骤1中检测到的危险触发器结合 — 将严重性提升一级。

Pass 3: action pinning

步骤3:Action固定方式

Look at every
uses:
line.
  • Pinned to a tag (
    uses: actions/checkout@v4
    ,
    @v4.1.1
    ) — P1. Tags are mutable; an attacker who compromises the action repo can force-push the tag.
  • Pinned to a branch (
    uses: actions/checkout@main
    ) — P0. Worse than tag pinning; any commit to that branch flows in instantly.
  • Pinned to a SHA but no version comment — P3 style finding. Recommend the format
    uses: owner/action@<sha> # v4.1.1
    so reviews of pin updates stay legible.
  • Pinned to a SHA that looks unusual (third-party action, suspicious owner, recently created repo) — flag for manual verification; can't confirm impostor-commit status without the GitHub API, but worth noting.
The rewrite for tag/branch pinning is always: replace with the full 40-character commit SHA of the version they intended, plus a
# vX.Y.Z
comment.
查看每一行
uses:
指令。
  • 固定到标签
    uses: actions/checkout@v4
    @v4.1.1
    )— P1级风险。标签是可变的;攻击者攻陷Action仓库后可强制推送标签。
  • 固定到分支
    uses: actions/checkout@main
    )— P0级风险。比标签固定更危险;分支的任何提交都会立即生效。
  • 固定到SHA但无版本注释 — P3级风格问题。建议使用格式
    uses: owner/action@<sha> # v4.1.1
    ,以便在审查SHA更新时保持可读性。
  • 固定到异常SHA(第三方Action、可疑所有者、近期创建的仓库)— 标记需手动验证;无需GitHub API即可确认伪造提交状态,但值得注意。
标签/分支固定的重写方案始终是:替换为目标版本的完整40位提交SHA,并添加
# vX.Y.Z
注释。

Pass 4: shell injection (template injection)

步骤4:Shell注入(模板注入)

For every
run:
block, scan for
${{ ... }}
substitutions.
Constant or non-attacker-controlled values are fine (e.g.
${{ matrix.os }}
,
${{ secrets.MY_TOKEN }}
though even that's risky in some contexts). The dangerous fields are:
  • github.event.pull_request.title
    ,
    body
    ,
    head.ref
    ,
    head.sha
    ,
    head.label
  • github.event.issue.title
    ,
    body
  • github.event.comment.body
    ,
    user.login
  • github.event.review.body
  • github.head_ref
  • github.event.workflow_run.head_branch
    ,
    head_commit.message
  • github.event.commits.*.message
    ,
    author.name
    ,
    author.email
  • Any
    inputs.*
    from
    workflow_dispatch
    if the workflow runs in a privileged context
  • Any field that ultimately came from
    actions/github-script
    , downloaded artifacts, or external API responses
Detection rule: if a
run:
block contains
${{ github.event.* }}
or
${{ github.head_ref }}
directly in the script body, that's P0 template injection. The fix is always:
yaml
undefined
扫描每个
run:
块中的
${{ ... }}
替换语法。
常量或非攻击者可控的值是安全的(例如
${{ matrix.os }}
${{ secrets.MY_TOKEN }}
,尽管在某些场景下仍有风险)。危险字段包括:
  • github.event.pull_request.title
    body
    head.ref
    head.sha
    head.label
  • github.event.issue.title
    body
  • github.event.comment.body
    user.login
  • github.event.review.body
  • github.head_ref
  • github.event.workflow_run.head_branch
    head_commit.message
  • github.event.commits.*.message
    author.name
    author.email
  • 特权上下文运行的
    workflow_dispatch
    中的任何
    inputs.*
  • 最终来自
    actions/github-script
    、下载产物或外部API响应的任何字段
检测规则: 如果
run:
块中直接在脚本体内包含
${{ github.event.* }}
${{ github.head_ref }}
,则为P0级模板注入漏洞。修复方案始终是:
yaml
undefined

vulnerable

存在漏洞

  • run: echo "Branch is ${{ github.head_ref }}"
  • run: echo "Branch is ${{ github.head_ref }}"

safe

安全写法

  • env: BRANCH: ${{ github.head_ref }} run: echo "Branch is $BRANCH"

Also flag (P1):

- `echo "VAR=${{ untrusted }}" >> $GITHUB_ENV` — environment file injection. The attacker can break out of the variable by including newlines.
- `echo "::set-env name=VAR::${{ untrusted }}"` — deprecated workflow command, same problem.
- Inline scripts that `cat` an untrusted file into `$GITHUB_ENV` or `$GITHUB_OUTPUT`.
  • env: BRANCH: ${{ github.head_ref }} run: echo "Branch is $BRANCH"

同时标记(P1级):

- `echo "VAR=${{ untrusted }}" >> $GITHUB_ENV` — 环境文件注入。攻击者可通过换行符突破变量限制。
- `echo "::set-env name=VAR::${{ untrusted }}"` — 已弃用的工作流命令,存在相同问题。
- 内联脚本将不可信文件内容写入`$GITHUB_ENV`或`$GITHUB_OUTPUT`。

Pass 5: untrusted checkout

步骤5:不可信代码检出

For every
actions/checkout
step:
  • ref: ${{ github.event.pull_request.head.sha }}
    (or
    head.ref
    ) inside a workflow triggered by
    pull_request_target
    or
    workflow_run
    — P0. This is the canonical pwn-request: privileged context running fork-author code.
  • persist-credentials: true
    (default) on workflows that don't need to push back — P2. Recommend
    persist-credentials: false
    unless the workflow explicitly needs the embedded token.
查看每个
actions/checkout
步骤:
  • pull_request_target
    workflow_run
    触发的工作流中使用
    ref: ${{ github.event.pull_request.head.sha }}
    (或
    head.ref
    )— P0级风险。这是典型的pwn-request攻击:特权上下文运行分支作者的代码。
  • 不需要回推的工作流中使用
    persist-credentials: true
    (默认值)— P2级风险。建议设置
    persist-credentials: false
    ,除非工作流明确需要嵌入的token。

Pass 6: caching in privileged contexts

步骤6:特权上下文缓存

For every step using
cache:
input (most commonly on
actions/setup-node
,
setup-python
,
setup-go
,
setup-java
, or direct
actions/cache
):
  • Cache in a release or publish workflow — P0. Cache poisoning from any other workflow on the default branch can flow malicious build inputs into release. The Trivy and TeamPCP attacks both routed through this.
  • Cache in a workflow that handles secrets — P1.
  • Cache where the key isn't scoped to prevent untrusted PR workflows writing the same key as default-branch builds — P2.
The rewrite for release workflows: remove
cache:
entirely, add a comment explaining why (e.g.
# Do not cache: see https://github.com/actions/setup-node/issues/1445
).
查看每个使用
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
)。

Pass 7: artifact-borne injection

步骤7:产物注入

If the workflow downloads artifacts from another workflow (
actions/download-artifact
,
dawidd6/action-download-artifact
, etc.):
  • Artifact contents used in
    run:
    or
    $GITHUB_ENV
    without validation
    — P0 if the producing workflow runs on untrusted code (e.g.
    pull_request
    from forks). An attacker can put arbitrary content in the artifact.
  • Recommend strict validation: if the artifact is supposed to be a PR number, reject anything that isn't digits. If it's a structured file, parse and validate the schema.
如果工作流从其他工作流下载产物(
actions/download-artifact
dawidd6/action-download-artifact
等):
  • 产物内容未经验证就用于
    run:
    $GITHUB_ENV
    — 若生成产物的工作流运行不可信代码(例如来自分支的
    pull_request
    ),则为P0级风险。攻击者可在产物中植入任意内容。
  • 建议严格验证:如果产物应为PR编号,拒绝非数字内容;如果是结构化文件,解析并验证其 schema。

Pass 8: release-specific hardening

步骤8:发布流程专项加固

If the workflow looks like a release/publish workflow (publishes to npm, PyPI, crates.io, Docker registries; creates GitHub releases; pushes tags):
  • No
    environment:
    declared on the publish job
    — P1. Release credentials should be scoped to a deployment environment, not repo/org secrets.
  • Uses long-lived registry tokens (
    secrets.NPM_TOKEN
    ,
    secrets.PYPI_TOKEN
    ) instead of OIDC/Trusted Publishing — P2. Recommend the OIDC path for the relevant registry.
  • No attestation generation (
    actions/attest-build-provenance
    ,
    --provenance
    for npm, PEP 740 for PyPI) — P3 hardening recommendation, not a vulnerability.
  • Caching anywhere in the release path — P0, see Pass 6.
如果工作流属于发布/发布流程(发布到npm、PyPI、crates.io、Docker镜像仓库;创建GitHub发布;推送标签):
  • 发布作业未声明
    environment:
    — P1级风险。发布凭证应限定在部署环境中,而非仓库/组织密钥。
  • 使用长期注册表token
    secrets.NPM_TOKEN
    secrets.PYPI_TOKEN
    )而非OIDC/可信发布 — P2级风险。建议为对应注册表使用OIDC方案。
  • 未生成证明
    actions/attest-build-provenance
    、npm的
    --provenance
    、PyPI的PEP 740)— P3级加固建议,不属于漏洞。
  • 发布流程中存在缓存 — P0级风险,参见步骤6。

Pass 9: self-hosted runners

步骤9:自托管运行器

If the workflow uses
runs-on:
with anything other than GitHub-hosted runners (
ubuntu-*
,
windows-*
,
macos-*
):
  • Self-hosted runner reachable by fork PRs — P0. Self-hosted runners share state across jobs and have produced critical compromises (PyTorch). Flag for manual review of runner scoping.
  • This is outside the default threat model — note the finding and recommend the user verify runner restrictions in GitHub settings.
如果工作流的
runs-on:
使用GitHub托管运行器之外的环境(
ubuntu-*
windows-*
macos-*
):
  • 自托管运行器可被分支PR访问 — P0级风险。自托管运行器在作业间共享状态,已导致严重攻击事件(如PyTorch)。标记需手动审查运行器范围配置。
  • 此场景超出默认威胁模型范围——标注检测结果并建议用户在GitHub设置中验证运行器限制。

Finding format

检测结果格式

Report each finding in this structure. Group by severity, P0 first.
[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"
If the user pastes raw YAML without a filename, refer to it as "the workflow" and use line numbers within the snippet.
按以下结构报告每个检测结果。按严重性分组,P0级优先。
[P0] template-injection in .github/workflows/ci.yml:23
  Run块直接将github.event.pull_request.title插值到shell中。
  攻击者可控制PR标题,并在拥有GITHUB_TOKEN权限的工作流上下文中执行任意代码。

  漏洞代码:
    - run: echo "Title: ${{ github.event.pull_request.title }}"

  修复方案:
    - env:
        TITLE: ${{ github.event.pull_request.title }}
      run: echo "Title: $TITLE"
如果用户粘贴无文件名的原始YAML代码,将其称为“该工作流”并使用代码片段中的行号。

Severity scale

严重性等级

  • P0 — exploitable now, no chain needed. Fork PR authors or arbitrary GitHub users can compromise secrets, repo contents, or releases. Block merge.
  • P1 — exploitable with one extra step (e.g. requires combining with another finding, or requires a maintainer mistake). Block merge if found on a release path.
  • P2 — hardening gap. Not exploitable directly, but reduces blast radius if combined with a future bug. Fix in normal review cycle.
  • P3 — style or consistency finding. Worth fixing for legibility, no security impact.
  • P0 — 当前可被利用,无需额外攻击链。分支PR作者或任意GitHub用户可攻陷密钥、仓库内容或发布流程。需阻止合并。
  • P1 — 需额外一步才能被利用(例如需结合其他检测结果,或需维护者失误)。若在发布路径中发现,需阻止合并。
  • P2 — 加固缺口。无法直接被利用,但会在未来出现漏洞时扩大影响范围。需在常规审查周期中修复。
  • P3 — 风格或一致性问题。修复有助于提升可读性,无安全影响。

When the workflow looks fine

当工作流无问题时

After walking all nine passes, if nothing fires:
  1. Say so explicitly. "No findings against the standard rule set."
  2. Note what wasn't checked: org-level settings (default token permissions, ruleset enforcement, 2FA), repo-level settings (branch protection, tag protection, immutable releases), action source code (whether the pinned actions themselves install mutable binaries at runtime), and runtime behavior of dependencies.
  3. Recommend the user check the items in
    references/checklist.md
    under "Per repository" and "Per organization" — those need GitHub settings access, not workflow YAML.
A clean workflow scan does not mean a clean security posture.
完成所有9个步骤检查后,若未发现问题:
  1. 明确告知用户:“未发现违反标准规则集的问题。”
  2. 说明未检查的内容:组织级设置(默认token权限、规则集执行、双因素认证)、仓库级设置(分支保护、标签保护、不可变发布)、Action源代码(固定的Action是否在运行时安装可变二进制文件)、依赖的运行时行为。
  3. 建议用户查看
    references/checklist.md
    中“每个仓库”和“每个组织”下的内容——这些需要GitHub设置权限,而非工作流YAML。
工作流扫描无问题并不代表安全态势无风险。

Reference files

参考文件

  • references/triggers.md
    — detailed table of every GitHub Actions trigger, what makes each dangerous or safe, and the safe pattern for common things people use the dangerous ones for. Read this when a workflow uses a trigger the user is asking about specifically, or when you want to explain why a trigger is dangerous beyond the one-line summary.
  • references/checklist.md
    — flat per-workflow / per-repo / per-org checklist. Useful when the user asks for a full audit, when scanning many repos, or when triaging at scale. Includes triage priority order.
  • references/patterns.md
    — common "I want to do X safely" patterns. Read when the user asks how to replace a flagged dangerous pattern, not just identify it.
  • references/triggers.md
    — 详细列出每个GitHub Actions触发器、其危险/安全的原因,以及常见危险触发器的安全替代方案。当用户询问工作流使用的触发器,或需要解释触发器危险性的详细原因时,参考此文件。
  • references/checklist.md
    — 按工作流/仓库/组织分类的扁平化检查清单。适用于用户请求全面审计、扫描多个仓库或大规模分类处理场景。包含分类优先级顺序。
  • references/patterns.md
    — 常见“我想安全地实现X”的模式。当用户询问如何替换标记的危险模式(而非仅识别)时,参考此文件。

What this skill won't do

此技能不会执行的操作

  • It won't install zizmor, pinact, or anything else. The scan is the model reading the YAML and applying these rules.
  • It won't recommend installing tools unless the user explicitly asks "what tools should I run." Even then, point to the patterns directly — the rules in this skill are the audits those tools encode.
  • It won't paper over a
    pull_request_target
    finding with mitigations. If a workflow uses that trigger and isn't behind a GitHub App, it's an open finding.
  • It won't tell the user everything is fine without naming what was checked and what wasn't. Default answer to "is this safe" is "here's what the scan covers and here's what it found."
  • 不会安装zizmor、pinact或其他工具。扫描仅需模型读取YAML并应用上述规则。
  • 除非用户明确询问“我应该运行哪些工具”,否则不会推荐安装工具。即使用户询问,也直接指向相关模式——此技能中的规则已整合了这些工具的审计逻辑。
  • 不会用缓解措施掩盖
    pull_request_target
    的检测结果。如果工作流使用该触发器且未通过GitHub App防护,则视为未解决的问题。
  • 不会在未说明检查范围的情况下告知用户一切安全。对于“这是否安全”的默认回答是“以下是扫描覆盖的范围及检测结果”。