← 返回蜂巢洞察

如何防止GitHub Actions中的依赖项出现问题/被恶意代码污染

你们的工作流程使用了 actions/checkout@v4 这一标签。目前,这个标签指向的是经过审核的版本代码;但明天,如果维护者或攻击者篡改了这个标签,它就会指向恶意代码。而你们的集成管道在运行时会使用 GITHUB_TOKEN 来访问这些代码。 第三方提供的 Actions 实际上属于依赖项。这种“浮动标签”的机制就类似于未固定的包依赖关系——如果引用地址发生变化,工作流程也会执行不同的代码,但你们的仓库本身并不会因此发生任何改变。 本文会教你如何通过将Actions绑定到具体的提交哈希值上来防止连续集成过程中的安全风险,包括验证这些绑定的准确性、限制允许执行的操作类型,以及利用Depe

你们的工作流程使用了actions/checkout@v4这一标签。目前,这个标签指向的是经过审核的版本代码;但明天,如果维护者或攻击者篡改了这个标签,它就会指向恶意代码。而你们的集成管道在运行时会使用GITHUB_TOKEN来访问这些代码。

第三方提供的Actions实际上属于依赖项。这种“浮动标签”的机制就类似于未固定的包依赖关系——如果引用地址发生变化,工作流程也会执行不同的代码,但你们的仓库本身并不会因此发生任何改变。

本文会教你如何通过将Actions绑定到具体的提交哈希值上来防止连续集成过程中的安全风险,包括验证这些绑定的准确性、限制允许执行的操作类型,以及利用Dependabot自动处理需要审核的更新请求。

适用对象:负责保障GitHub Actions工作流程安全的平台工程师及DevSecOps专家。

前提条件:

  • 拥有仓库管理权限

  • 使用uses: org/action@ref语法配置工作流程

快速参考指南:

以下是你们需要掌握的内容:

  • 将所有uses:引用绑定到具体的提交哈希值上,而不是像@v4这样的浮动标签。

  • 在批准任何更新之前,务必验证所使用的提交哈希值是否与对应的发布版本相匹配。

  • 在组织或仓库层面,制定明确的允许执行的操作列表。

  • 对于需要审核的版本更新,务必启用Dependabot来进行自动化处理。

  • 优先选择官方提供的或经过验证的第三方Actions;如有必要,可以在内部复制那些关键的操作流程。

  • 在合并拉取请求之前,一定要检查这些绑定的有效性。

目录结构

为什么浮动标签会失效

GitHub Actions允许你使用分支名称、版本标签或提交哈希值来引用依赖项。然而,这些引用方式并不能提供同样的稳定性:分支随时都可能发生变化,而版本标签也可能在审核后被修改为指向其他提交。只有提交哈希值才能唯一地标识某个具体的代码版本。

下表对比了常见的引用格式,并说明了为什么使用可移动的标签会带来供应链风险:

引用格式 风险
@main 当工作流开始时,会执行该分支上的所有代码
@v4 这个标签是可移动的,因此同一个标签可能会用来执行不同的代码
@v4.2.1 虽然更具体,但仍然属于可变的标签类型
@abc1234... (完整的SHA值) 用于标识一个不可变的提交记录

前三种引用方式都是不正确的,因为这些标签名称并不能永久地确定GitHub应该执行哪段代码。而使用完整的SHA值就可以解决这个问题,因为只有当有人在仓库中修改了相应的引用信息时,工作流才会发生变化。

关键要点:持续集成管道也应该遵循与应用代码相同的依赖管理规范。

将操作与具体的提交SHA值关联起来

将某个操作与其对应的提交SHA值关联起来,意味着会用你审核过的那个提交的完整SHA值来替代该操作所引用的分支或版本标签。这样一来,即使原标签后来被修改了,GitHub也会继续使用你最初指定的SHA值来执行相应的操作。

第一个例子是错误的,因为这两个引用都使用了可变的版本标签。这样,工作流在未来可能会执行不同的代码,而你的仓库中却没有任何相应的更改:

- uses: actions/checkout@v4
- uses: actions/setup-node@v4

第二个例子是正确的,因为每个引用都使用了完整的40位SHA值。虽然注释中写的是易于理解的版本号,但GitHub实际上使用的是SHA值来进行匹配:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@4992456781334f2795ae9dd169f5041797d85689 # v4.4.0

你可以在该操作的GitHub发布页面上找到对应的SHA值,或者使用以下命令来获取它:

gh api repos/actions/checkout/git/ref/tags/v4.2.2 --jq '.object.sha'

请务必使用你实际审核过的那个提交标签。为了便于维护,可以在注释中写明该版本的详细信息。这些注释虽然对审核者有帮助,但并不属于安全控制机制的一部分。GitHub最终会执行由SHA值标识的那个提交记录。

千万不要使用缩写的SHA值。完整的40位SHA值能有效降低发生冲突或导致审核错误的可能性。

验证提交的SHA值

虽然完整的SHA值可以防止你在合并代码后标签位置发生变化,但在批准某个提交之前,仍然需要先验证其SHA值的准确性。发布标签和对应的提交记录应该一起进行审核。这个检查会对比你审核过的标签所对应的提交记录与工作流中记录的提交信息:

#!/usr/bin/env bash
set -euo pipefail

repository="actions/checkout"
release_tag="v4.2.2"
expected_sha="11bd71901bbe5b1630ceea73d27597364c9af683"

actual_sha="$({
  git ls-remote "https://github.com/${repository}.git" \
    "refs/tags/${release_tag}" "refs-tags/${release_tag}^{}"
} | awk -v tag="refstags/${release_tag}" '
  $2 == tag "^{}" { print $1; found = 1 }
  $2 == tag { fallback = $1 }
  END { if (!found) print fallback }
')"

if [[ "${actual_sha}" != "${expected_sha}" ]]; then
  printf 'SHA值不匹配:%s %s\n' "${repository}" "${release_tag}" >&2
  printf '预期值:%s\n实际值:%s\n' "${expected_sha}" "${actual_sha}" >&2
  exit 1
fi

printf '验证通过:%s@%s\n' "${repository}" "${expected_sha}"

这仅仅是一种版本验证机制,并不意味着某个仓库一定是可信赖的。请仔细阅读相关操作的来源信息,审查其权限设置,并记录下自己为何批准使用该依赖项。在需要更高安全性的环境中,可以在内部创建这些关键操作的镜像版本,然后通过常规的仓库管理流程来验证这些镜像文件的有效性。

为GitHub Actions启用Dependabot

Dependabot是GitHub提供的自动化依赖项更新服务。对于GitHub Actions来说,它会检查工作流中引用的依赖项是否为最新版本,并自动创建拉取请求来更新这些依赖项。这样一来,你就可以在一个便于审核的平台上查看并批准这些更新操作,而无需手动修改相关配置或使用临时标签。

请创建文件.github/dependabot.yml,内容如下:

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: ""
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"

当被标记为“固定依赖项”的SHA版本有新更新时,Dependabot会自动创建拉取请求。你可以像处理应用程序的依赖项更新一样,对这些请求进行审核并合并它们。

在建立完善的审核流程之前,请不要将Dependabot设置为自动合并这些更新。因为新的更新内容可能包含新的权限设置、修改后的脚本,或是新的依赖关系。

对于每一个更新拉取请求,都需要对比旧版本和新版本的代码差异,查看该操作的发布说明,并检查action.yml文件中JavaScript代码包、shell脚本以及工作流权限设置是否发生了变化。要确保新版本确实属于你打算采用的那个版本。如果该操作涉及部署、发布、管理凭证等高影响操作,请在测试仓库或环境中先运行该工作流进行验证。

我们的目标并不是拒绝所有的更新请求,而是要让决策过程更加透明且可重复执行。审核者在合并更新之前,应该能够回答以下三个问题:哪些代码发生了变化?为什么需要使用新版本?这些新代码会拥有哪些权限?

为特定操作设置允许列表

操作允许列表是一种仓库或组织级别的策略,用于限制哪些发布者或仓库的工作流可以被调用。这样做可以降低贡献者将未知或未经审核的操作引入到受信任的工作流程中的风险。

组织管理员可以在设置 → Actions → 策略 → 允许指定的操作中配置这一规则。请选择与你的工作流程最匹配的策略选项,然后只添加那些已经被团队审核过的仓库。

一些示例允许列表模式如下:

actions/checkout@*
actions/setup-node@*
actions/cache@*
docker/*
my-org/*

按照这种配置,不符合允许列表模式的操作将会被拒绝。属于该组织的所有仓库都会继承这一策略,不过具体实施效果还会受到组织所使用的GitHub计划及仓库设置的影响。

对于没有组织级策略的单个仓库,可以在设置 → Actions → 通用设置 → 允许GitHub创建的操作,并选择非GitHub来源的操作中,仅允许经过验证的创建者来提交这些操作请求。

允许列表用于限制哪些操作身份可以执行特定的操作。它并不能替代SHA校验机制。在任何工作流程中,被允许执行的操作仍然需要通过经过审核的完整SHA值来进行验证。

在拉取请求中强制使用SHA校验

在持续集成环境中运行工作流检查工具,并添加一项仓库检测规则:如果某个工作流程中出现了非SHA格式的引用,该检测规则就会报错。在将某项检查工具用于受保护的工作流程之前,先将其自身的SHA值进行固定。请用您所选的操作检查工具发布的完整提交SHA值替换以下占位符:

name: actionlint

on:
  pull_request:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
      - uses: rhysd/actionlint@03d0035246f3e81f36aed592ffb4bebf33a03106 # v1.7.7
        with:
          args: -color

与处理actions/checkout时一样,需要解析版本标签,审核相关提交信息,并记录下那40个字符长的完整SHA值。

您也可以使用简单的检查机制来发现那些常见的错误引用。不过,请将这种检查视为一种辅助手段,而不是依赖它来确保YAML格式的正确性或整个供应链的安全性:

      - name: 拒绝使用错误的操作标签
        run: |
          if grep -RInE '^[[:space:]]*-?[[:space:]]*uses:[[:space:]]*[^#]+@(main|master|v[0-9]+)([[:space:]]|$)' .github/workflows/; then
            echo "发现错误的操作标签。请使用完整的SHA值进行校验。"
            exit 1
          fi

这种检查机制应该同时应用于拉取请求和受保护的分支上。因为即使某个拉取请求通过了这项检查,其中仍可能包含恶意提交,因此还需要结合代码审核、允许列表以及SHA值验证流程来确保安全。

审查第三方操作

在将某个社区提供的操作添加到允许列表之前,请先执行以下步骤:

  1. 阅读该操作的action.yml文件及其入口脚本。

  2. 查看该操作的星标数量、维护者的声誉以及是否存在未解决的安全问题。

  3. 如果该操作非常重要但由外部团队维护,那么请将其SHA值固定下来,并将其分支克隆到my-org/action-name目录中。

同时还需要检查该工作流程所拥有的权限。那些能够读取仓库机密信息或修改发布内容的操作,显然需要比那些仅仅用于格式化文件的操作受到更严格的审查。因此,请将工作流程的顶级权限设置为最低限度的必要权限,只有在确实需要的情况下,才为具体的任务授予额外的权限。

如何验证这些措施的有效性

  1. 对于您批准使用的每一个操作,都要运行SHA值验证脚本,并确保所有的比较结果都是正确的。

  2. 创建一个测试拉取请求,将某个操作的标签修改为@main,然后确认持续集成系统会拒绝这个请求。

  3. 在错误的操作标签后面添加注释,然后确认您的解析工具或检查工具仍然会拒绝接受这种格式的标签。

  4. 验证组织政策是否能够阻止那些被禁止的操作在测试工作流程中被执行。

  5. 确认Dependabot是否会自动触发操作更新拉取请求,以及这些变更是否能正常经过审核流程。

  6. 检查工作流程的权限设置,确保这些操作无法访问它们并不需要的敏感信息。

当这种方法出现问题的时候

  1. 紧急补丁:使用SHA值进行固定会延缓热修复的速度。依赖检测工具加上值班人员的审核,比临时使用临时标签更为安全。

  2. 私有仓库中的复合操作:对于私有仓库中的内部操作,也应与公开仓库中的操作一样,使用SHA值进行固定。

  3. 可重用的工作流程:不仅要将可重用的工作流程的引用地址用SHA值固定下来,其中包含的各个步骤也都需要进行同样的处理。

  4. 带注释的标签与镜像:在记录SHA值之前,需要先将带注释的标签解析为对应的提交信息;同时,还需要通过自身的变更控制流程来验证内部镜像的合法性。

  5. 虚假的安全感:

    即使使用了SHA值进行固定,但如果不实际阅读代码,仍然会信任在固定时指定的维护者。因此,应该将SHA值固定与允许列表、最小权限设置以及审核机制结合起来使用。

结论

通过本教程,您学会了如何通过将GitHub Actions与经过审核的提交信息中的SHA值关联起来、验证这些引用地址、仅允许特定的操作、启用依赖检测工具的更新功能,以及在拉取请求检查中强制执行这些固定措施,从而防止CI依赖关系出现安全问题。

这样就能形成多层次的安全控制机制:经过审核的不可更改的引用地址、受限制的操作列表、自动化的更新提示,以及能够在合并之前发现回归问题的拉取请求检查机制。

参考资料

相关文章

技术实践

如何使用JavaScript开发一款可将基于浏览器的图像故事板转换为PDF格式的工具

在任何人开始拍摄视频、制作广告或绘制漫画的最终画面之前,通常会先画出一份故事板草图:按照拍摄顺序为每一镜头绘制一幅图片,并附上简短的说明,说明该镜头中发生了什么以及持续时间有多长。 大多数人已经拥有这些图片了——它们可能是手机照片、截图、粗略的草图,或者是通过人工智能生成的图像文件,这些文件都保存在某个文件夹里。但他们所缺乏的是一种能够将这些文件快速整理成格式规范、编号整齐的文档,并将其打印出来或发送给客户的工具。 在这个教程中,你将学习如何制作这样一个基于浏览器的工具。你需要添加一组图片,为每张图片指定标题、描述和持续时间,选择合适的布局方式,预览生成的文档,然后下载可供打印的PDF文件。

阅读全文
技术实践

由 TypeScript 7 提供支持的 Angular 编译器已进入测试阶段,该编译器配备了基于 Rust 的预处理工具。

Angular团队推出了ngp这一编译器预处理器,该工具利用Oxc重新实现了Rust语言中的分析阶段处理流程。它取代了ngc对Transformer API的依赖——而TypeScript 7.0已经移除了对Transformer API的支持。早期的测试结果显示,使用ngp进行冷构建时,编译速度可以提高2到3倍。预计这一新技术将在Angular 22的次要版本中正式被采用;在此之前,生产环境仍然会继续使用ngc进行编译。 作者:Daniel Curtis

阅读全文
技术实践

谷歌云的支出上限功能现已通过API接口正式推出,并允许用户每月进行重置设置。

Google Cloud现已全面推出了针对云账单预算的使用额度限制功能:一旦某个项目的月度预算被用完,相关服务的新增使用请求将会被暂停,但现有资源不会被删除。该系统提供了API接口、预估费用功能以及自动每月重置机制;不过这些额度限制仅适用于Gemini API、Agent Platform、Cloud Run以及Cloud Run Functions等服务。在限制措施生效期间,如果仍出现超支情况,相关费用仍然会正常计费。 作者:Steef-Jan Wiggers

阅读全文
技术实践

如何使用JavaScript构建PDF故事板生成工具

PDF文件常被用来共享演示文稿、脚本、报告、教学材料、漫画页以及其他视觉文档。但若要逐页查看长度较长的PDF文件,将会耗费大量时间,尤其是当人们需要了解所有场景、幻灯片或视觉内容的整体顺序时。 故事板提供了一种更为有条理的方式来查看这些信息。用户无需逐一打开每一页,而是可以将选中的PDF页面整理成一张包含多个面板的单页。每个面板可以显示页码、标题以及原始页面的可视化预览图。 在本教程中,我们将学习如何使用JavaScript、PDF.js和PDF-lib来构建一个基于浏览器的PDF故事板生成工具。 该应用程序会从现有的PDF文件中提取页面内容,并将它们整理成可供打印的故事板页面。用户可以选择不

阅读全文