关于 ci-cd-security
CI/CD Security Scanner 是用于审查 GitHub Actions 工作流 YAML 安全问题的技能。它按既定规则检测并给出严重级别与具体修订建议,无需安装或运行扫描工具。
主要功能
- 01审查工作流 YAML
- 02检查触发器和权限
- 03检测 Action 固定版本风险
- 04查找 Shell 模板注入
- 05检查缓存和制品处理
- 06提供具体改写建议
适用场景
- 01合并前审查工作流
- 02审计发布工作流
- 03检查 GitHub Actions 安全问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ superagent-ai/ skills --skill ci-cd-security安装后在 Claude Code 中输入/ci-cd-security确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/checklist.mdpatterns.mdtriggers.md详细介绍
CI/CD 安全扫描器
此技能会将模型转变为工作流 YAML 扫描器。读取文件,逐一检查检测规则,并报告发现的问题、严重性和具体的改写方案。无需安装工具,也无需运行命令——分析过程就是模型读取 YAML。
这些规则体现了 Astral、OpenSSF、GitHub Security Lab、Chainguard 和 zizmor 审计规则集目前达成的共识。目标是在无需运行这些工具的情况下,标记出它们也会标记的相同模式。
心智模型
每个工作流都处于一个 2×2 矩阵中:特权与非特权,以及受信任代码与不受信任代码。只有一个单元格会导致遭到入侵:特权工作流运行不受信任的代码。以下规则用于检测工作流何时落入该单元格。
- 特权 = 拥有密钥、写入权限,或会生成敏感构件(发布、部署、评论、标签)。
- 不受信任代码 = 分叉 PR 作者能够影响的任何内容:PR 源代码、PR 标题、PR 正文、提交消息、分支名称、工作流读取的文件、缓存、由其他不受信任的工作流生成的构件。
无法确定某个值是否受信任时,就将其视为不受信任。误报的代价是一条代码审查意见;漏报的代价则是供应链遭到入侵。
扫描流程
对于用户提供的每个工作流文件,按顺序逐一执行以下检查。每项检查对应一类攻击。
第 1 项:危险触发器
检查 on: 块。立即标记:
pull_request_target— P0,除非有明确的合理理由。该触发器会使用密钥和写入权限运行,分叉 PR 可以触发它。这是典型的 pwn-request 攻击途径。即使没有对 head 执行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 模板注入。修复方式始终是:
# vulnerable
- run: echo "Branch is ${{ github.head_ref }}"
# safe
- 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 步骤:
- 在由
pull_request_target或workflow_run触发的工作流中使用ref: ${{ github.event.pull_request.head.sha }}(或head.ref)—— P0。这是典型的恶意请求:在特权上下文中运行来自分叉仓库作者的代码。 - 在不需要推送回去的工作流中使用
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— 如果生成工件的工作流在不受信任的代码上运行(例如来自分叉仓库的pull_request),则为 P0。攻击者可以在工件中放入任意内容。 - 建议进行严格验证:如果工件应当是 PR 编号,则拒绝任何非数字内容。如果是结构化文件,则解析并验证其架构。
第 8 轮:发布专属加固
如果工作流看起来是发布/推送工作流(发布到 npm、PyPI、crates.io、Docker 注册表;创建 GitHub 发布;推送标签):
- 发布作业未声明
environment:— P1。发布凭据应限定在部署环境中,而不是仓库/组织密钥。 - 使用长期有效的注册表令牌(
secrets.NPM_TOKEN、secrets.PYPI_TOKEN),而不是 OIDC/Trusted Publishing — P2。建议使用相应注册表的 OIDC 方式。 - 未生成证明(
actions/attest-build-provenance、npm 的--provenance、PyPI 的 PEP 740)— P3 加固建议,不属于漏洞。 - 在发布流程的任何环节使用缓存 — P0,参见第 6 轮。
第 9 轮:自托管运行器
如果工作流的 runs-on: 使用的不是 GitHub 托管的运行器(ubuntu-*、windows-*、macos-*):
- 分叉 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 — 可立即利用,无需组合其他条件。分叉 PR 作者或任意 GitHub 用户可危及密钥、仓库内容或发布。阻止合并。
- P1 — 需要额外一步才能利用(例如需要与其他发现项组合,或需要维护者操作失误)。如果在发布流程中发现,则阻止合并。
- P2 — 加固缺口。无法直接利用,但若与未来的漏洞组合,会扩大影响范围。在常规审查周期内修复。
- P3 — 风格或一致性问题。为提高可读性,值得修复;不影响安全。
工作流看起来没有问题时
完成全部九轮检查后,如果没有发现任何问题:
- 明确说明这一点。“未发现违反标准规则集的问题。”
- 说明未检查的内容:组织级设置(默认令牌权限、规则集强制执行、双因素认证)、仓库级设置(分支保护、标签保护、不可变发布)、操作源代码(固定版本的操作本身是否会在运行时安装可变二进制文件),以及依赖项的运行时行为。
- 建议用户检查
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 之后,这就是一个未解决的问题。 - 它不会在不说明检查了什么、没检查什么的情况下告诉用户一切正常。对于“这样安全吗”这类问题,默认回答是“以下是扫描涵盖的内容,以及扫描发现的问题。”