ci-cd-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCI/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 block. Flag immediately:
on:- — P0 unless explicitly justified. Runs with secrets and write permissions, triggerable by fork PRs. The canonical pwn-request vector. Even without
pull_request_targetof head, attacker input shows up in PR title, branch name, commit messages, and gets interpolated.checkout - — P0. Same problem as
workflow_runbut indirect, via a chainedpull_request_targetworkflow's artifacts or metadata.pull_request - ,
issue_comment,issues,pull_request_review— 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.pull_request_review_comment - with broad wildcards (
pushor no branch filter) — P2. An attacker who lands a PR can fire a privileged workflow by pushing a follow-up branch.branches: ['*']
For each finding: name the trigger, explain why it's dangerous in this specific workflow's context, and propose the rewrite (usually , sometimes a split into two workflows, sometimes "this needs a GitHub App not Actions").
pull_request查看块,立即标记以下内容:
on:- — 除非有明确合理的理由,否则为P0级风险。该触发器会携带密钥和写入权限运行,可被分支PR触发,是典型的pwn-request攻击载体。即使不checkout头部分支,攻击者输入的PR标题、分支名称、提交信息等仍会被插值处理。
pull_request_target - — P0级风险。与
workflow_run存在相同问题,但攻击是间接的,通过链式pull_request_target工作流的产物或元数据触发。pull_request - 、
issue_comment、issues、pull_request_review— P1级风险。携带密钥运行,任何可评论的用户都能触发。仅当工作流未将用户可控字段插值到shell模板中时才安全。pull_request_review_comment - 使用通配符的触发器(
push或无分支过滤)— P2级风险。攻击者提交PR后,可通过推送后续分支触发特权工作流。branches: ['*']
对于每个检测结果:指明触发器名称,解释其在当前工作流场景中的危险性,并提出重写方案(通常替换为,有时拆分为两个工作流,有时建议“此场景需使用GitHub App而非Actions”)。
pull_requestPass 2: permissions
步骤2:权限配置
Look for blocks at workflow and job level.
permissions:- No top-level block — P1. The default
permissions:permissions depend on repo and org settings; can beGITHUB_TOKENon older repos. Flag with: "addwrite-allat top, grant per-job."permissions: {} - anywhere — P1.
permissions: write-all - Job-level granting more than the job clearly needs — P2. e.g.
permissions:on a job that only runs tests. Recommend least privilege.contents: write - Combined with a dangerous trigger from Pass 1 — escalate severity by one level.
查看工作流和作业级别的块。
permissions:- 无顶层块 — P1级风险。默认的
permissions:权限取决于仓库和组织设置;在旧仓库中可能为GITHUB_TOKEN。标记提示:“在顶层添加write-all,并为每个作业按需授权。”permissions: {} - 任何位置出现— P1级风险。
permissions: write-all - 作业级授予的权限超出作业实际需求 — P2级风险。例如,仅运行测试的作业设置了
permissions:。建议遵循最小权限原则。contents: write - 与步骤1中检测到的危险触发器结合 — 将严重性提升一级。
Pass 3: action pinning
步骤3:Action固定方式
Look at every line.
uses:- Pinned to a tag (,
uses: actions/checkout@v4) — P1. Tags are mutable; an attacker who compromises the action repo can force-push the tag.@v4.1.1 - Pinned to a branch () — P0. Worse than tag pinning; any commit to that branch flows in instantly.
uses: actions/checkout@main - Pinned to a SHA but no version comment — P3 style finding. Recommend the format so reviews of pin updates stay legible.
uses: owner/action@<sha> # v4.1.1 - 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 comment.
# vX.Y.Z查看每一行指令。
uses:- 固定到标签(、
uses: actions/checkout@v4)— P1级风险。标签是可变的;攻击者攻陷Action仓库后可强制推送标签。@v4.1.1 - 固定到分支()— P0级风险。比标签固定更危险;分支的任何提交都会立即生效。
uses: actions/checkout@main - 固定到SHA但无版本注释 — P3级风格问题。建议使用格式,以便在审查SHA更新时保持可读性。
uses: owner/action@<sha> # v4.1.1 - 固定到异常SHA(第三方Action、可疑所有者、近期创建的仓库)— 标记需手动验证;无需GitHub API即可确认伪造提交状态,但值得注意。
标签/分支固定的重写方案始终是:替换为目标版本的完整40位提交SHA,并添加注释。
# vX.Y.ZPass 4: shell injection (template injection)
步骤4:Shell注入(模板注入)
For every block, scan for substitutions.
run:${{ ... }}Constant or non-attacker-controlled values are fine (e.g. , though even that's risky in some contexts). The dangerous fields are:
${{ matrix.os }}${{ secrets.MY_TOKEN }}- ,
github.event.pull_request.title,body,head.ref,head.shahead.label - ,
github.event.issue.titlebody - ,
github.event.comment.bodyuser.login github.event.review.bodygithub.head_ref- ,
github.event.workflow_run.head_branchhead_commit.message - ,
github.event.commits.*.message,author.nameauthor.email - Any from
inputs.*if the workflow runs in a privileged contextworkflow_dispatch - Any field that ultimately came from , downloaded artifacts, or external API responses
actions/github-script
Detection rule: if a block contains or directly in the script body, that's P0 template injection. The fix is always:
run:${{ github.event.* }}${{ github.head_ref }}yaml
undefined扫描每个块中的替换语法。
run:${{ ... }}常量或非攻击者可控的值是安全的(例如、,尽管在某些场景下仍有风险)。危险字段包括:
${{ matrix.os }}${{ secrets.MY_TOKEN }}- 、
github.event.pull_request.title、body、head.ref、head.shahead.label - 、
github.event.issue.titlebody - 、
github.event.comment.bodyuser.login github.event.review.bodygithub.head_ref- 、
github.event.workflow_run.head_branchhead_commit.message - 、
github.event.commits.*.message、author.nameauthor.email - 特权上下文运行的中的任何
workflow_dispatchinputs.* - 最终来自、下载产物或外部API响应的任何字段
actions/github-script
检测规则: 如果块中直接在脚本体内包含或,则为P0级模板注入漏洞。修复方案始终是:
run:${{ github.event.* }}${{ github.head_ref }}yaml
undefinedvulnerable
存在漏洞
- 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 step:
actions/checkout- (or
ref: ${{ github.event.pull_request.head.sha }}) inside a workflow triggered byhead.reforpull_request_target— P0. This is the canonical pwn-request: privileged context running fork-author code.workflow_run - (default) on workflows that don't need to push back — P2. Recommend
persist-credentials: trueunless the workflow explicitly needs the embedded token.persist-credentials: false
查看每个步骤:
actions/checkout- 在或
pull_request_target触发的工作流中使用workflow_run(或ref: ${{ github.event.pull_request.head.sha }})— P0级风险。这是典型的pwn-request攻击:特权上下文运行分支作者的代码。head.ref - 不需要回推的工作流中使用(默认值)— P2级风险。建议设置
persist-credentials: true,除非工作流明确需要嵌入的token。persist-credentials: false
Pass 6: caching in privileged contexts
步骤6:特权上下文缓存
For every step using input (most commonly on , , , , or direct ):
cache:actions/setup-nodesetup-pythonsetup-gosetup-javaactions/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 entirely, add a comment explaining why (e.g. ).
cache:# Do not cache: see https://github.com/actions/setup-node/issues/1445查看每个使用输入的步骤(最常见于、、、或直接使用):
cache:actions/setup-nodesetup-pythonsetup-gosetup-javaactions/cache- 发布工作流中使用缓存 — P0级风险。默认分支上其他工作流的缓存投毒可将恶意构建输入传入发布流程。Trivy和TeamPCP攻击均通过此路径实现。
- 处理密钥的工作流中使用缓存 — P1级风险。
- 缓存键未限定范围,导致不可信PR工作流与默认分支构建使用相同缓存键 — P2级风险。
发布工作流的重写方案:完全移除,添加注释说明原因(例如)。
cache:# Do not cache: see https://github.com/actions/setup-node/issues/1445Pass 7: artifact-borne injection
步骤7:产物注入
If the workflow downloads artifacts from another workflow (, , etc.):
actions/download-artifactdawidd6/action-download-artifact- Artifact contents used in or
run:without validation — P0 if the producing workflow runs on untrusted code (e.g.$GITHUB_ENVfrom forks). An attacker can put arbitrary content in the artifact.pull_request - 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-artifactdawidd6/action-download-artifact- 产物内容未经验证就用于或
run:— 若生成产物的工作流运行不可信代码(例如来自分支的$GITHUB_ENV),则为P0级风险。攻击者可在产物中植入任意内容。pull_request - 建议严格验证:如果产物应为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 declared on the publish job — P1. Release credentials should be scoped to a deployment environment, not repo/org secrets.
environment: - Uses long-lived registry tokens (,
secrets.NPM_TOKEN) instead of OIDC/Trusted Publishing — P2. Recommend the OIDC path for the relevant registry.secrets.PYPI_TOKEN - No attestation generation (,
actions/attest-build-provenancefor npm, PEP 740 for PyPI) — P3 hardening recommendation, not a vulnerability.--provenance - Caching anywhere in the release path — P0, see Pass 6.
如果工作流属于发布/发布流程(发布到npm、PyPI、crates.io、Docker镜像仓库;创建GitHub发布;推送标签):
- 发布作业未声明— P1级风险。发布凭证应限定在部署环境中,而非仓库/组织密钥。
environment: - 使用长期注册表token(、
secrets.NPM_TOKEN)而非OIDC/可信发布 — P2级风险。建议为对应注册表使用OIDC方案。secrets.PYPI_TOKEN - 未生成证明(、npm的
actions/attest-build-provenance、PyPI的PEP 740)— P3级加固建议,不属于漏洞。--provenance - 发布流程中存在缓存 — P0级风险,参见步骤6。
Pass 9: self-hosted runners
步骤9:自托管运行器
If the workflow uses with anything other than GitHub-hosted runners (, , ):
runs-on: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.
如果工作流的使用GitHub托管运行器之外的环境(、、):
runs-on: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:
- Say so explicitly. "No findings against the standard rule set."
- 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.
- Recommend the user check the items in under "Per repository" and "Per organization" — those need GitHub settings access, not workflow YAML.
references/checklist.md
A clean workflow scan does not mean a clean security posture.
完成所有9个步骤检查后,若未发现问题:
- 明确告知用户:“未发现违反标准规则集的问题。”
- 说明未检查的内容:组织级设置(默认token权限、规则集执行、双因素认证)、仓库级设置(分支保护、标签保护、不可变发布)、Action源代码(固定的Action是否在运行时安装可变二进制文件)、依赖的运行时行为。
- 建议用户查看中“每个仓库”和“每个组织”下的内容——这些需要GitHub设置权限,而非工作流YAML。
references/checklist.md
工作流扫描无问题并不代表安全态势无风险。
Reference files
参考文件
- — 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/triggers.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/checklist.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/patterns.md
- — 详细列出每个GitHub Actions触发器、其危险/安全的原因,以及常见危险触发器的安全替代方案。当用户询问工作流使用的触发器,或需要解释触发器危险性的详细原因时,参考此文件。
references/triggers.md - — 按工作流/仓库/组织分类的扁平化检查清单。适用于用户请求全面审计、扫描多个仓库或大规模分类处理场景。包含分类优先级顺序。
references/checklist.md - — 常见“我想安全地实现X”的模式。当用户询问如何替换标记的危险模式(而非仅识别)时,参考此文件。
references/patterns.md
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 finding with mitigations. If a workflow uses that trigger and isn't behind a GitHub App, it's an open finding.
pull_request_target - 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并应用上述规则。
- 除非用户明确询问“我应该运行哪些工具”,否则不会推荐安装工具。即使用户询问,也直接指向相关模式——此技能中的规则已整合了这些工具的审计逻辑。
- 不会用缓解措施掩盖的检测结果。如果工作流使用该触发器且未通过GitHub App防护,则视为未解决的问题。
pull_request_target - 不会在未说明检查范围的情况下告知用户一切安全。对于“这是否安全”的默认回答是“以下是扫描覆盖的范围及检测结果”。