在我们之前的文章中, 我们了解了如何检测和预防直接中毒 Pipeline 执行(D-PPE)。我们还了解了如何使用 Xygeni 扫描仪,以及一些保护机制。
中毒 Pipeline 执行 (PPE)是在攻击者可以修改 pipeline 逻辑有两种:
- 通过修改 CI 配置文件( pipeline)-> 直接个人防护装备 (D-PPE)
- 通过修改 pipeline (例如:从 pipeline 配置文件)-> 间接个人防护装备 (I-PPE)
在这篇文章中,我们将深入探讨间接 PPE。但在此之前,作为我上一篇文章的补充,让我们先看看 GitHub 如何管理 pipeline针对 D-PPE 的保护机制是什么。
GitHub 如何保护 pipeline来自 PR 吗?
GitHub 如何执行修改后的 pipelines?
修改日期 pipeline可以来自 Pushes 或 Pull Requests (PR)。 作为一项重要的最佳实践,强烈建议避免直接“推送”到受保护的分支,并使用 Pull Requests 作为一种在接受任何贡献的代码之前强制进行一些审查的机制。
Pull Requests 可能来自两个不同的来源:
- PR 来自 叉子
- PR 来自 分支机构
PR 来自 叉子 可以来自 国家 or 私立 仓库。
由于我们正在处理 PPE(中毒 Pipeline 执行),我们的重点不是“接受”PR,而是执行修改后的 pipeline 在 PR 的接受/批准过程中。PPE 攻击的核心是意外执行“恶意”修改的 pipeline.
简而言之,中毒 Pipeline 执行(PPE)发生在 攻击者可以修改 pipeline 逻辑.
那里有两个 变种:
- 直接个人防护装备 (聚苯醚): 在 D-PPE 场景中, 攻击者修改 CI 配置文件 在他们可以访问的存储库中,要么将更改直接推送到存储库上未受保护的远程分支,要么从分支或分叉提交包含更改的 PR。 自 CI pipeline 执行由修改后的 CI 配置文件中的命令定义,攻击者的恶意命令最终在构建完成后在构建节点中运行 pipeline 被触发。
- 间接个人防护装备 (个人防护装备): 在某些情况下,如果对手能够接触到 SCM 存储库(例如,如果 pipeline 配置为从同一存储库中单独的受保护分支中提取 CI 配置文件)。 在这种情况下,与其毒害 pipeline 攻击者将恶意代码注入到 pipeline (例如:从 pipeline 配置文件)
在这两种情况下 GitHub 将执行修改后的 pipeline 无需事先审查或批准.
来自 fork 的 PR 国家 休息
GitHub 允许配置处理时的行为 来自公共存储库分支的 PR.
当 PR 来自一个 fork 时,GitHub 总是在执行之前强制进行一定程度的“批准”。 pipeline 与 PR 相关。这一级别的批准从弱批准变为严格批准。
At 组织级别 (组织>>设置>>操作>>常规),您可以在几个“批准”选项中做出选择:
最严格的是最后一个(“需要所有外部合作者的批准”),因为当 PR 来自外部合作者的 fork 时,GitHub 总是需要获得批准。
但即使在这种严格的情况下, 具有读写权限的协作者之间的差异.
- 当 PR 来自 读 用户, 执行 pipeline 已停止 直到变更获得批准。如果批准,则修改后的 pipeline 被执行。
- 当 PR 来自 写 用户, 无需批准,修改后的 pipeline 始终被执行!!
总之,来自公共存储库分支的 PR 受到的 PPE 保护较弱。对外部(读取)用户有一定的保护,但对内部(写入)用户没有任何保护。
关于什么 来自私人仓库分支的 PR?
来自 fork 的 PR 私立 休息
在这种情况下,GitHub 提供了一些有用的配置设置。
以上设置可以在 组织 或 回购 水平。
在规划婴儿食品行业的工艺要求时,安全性和可靠性是工艺设计中最重要的方面。 未选中任何选项,GitHub 将 请求批准 以及 它不会执行修改后的 pipeline。这是最安全的配置!!
此 最不安全的配置 当 “从 fork 运行工作流程 pull request”已选中。在这种情况下,对于读写用户来说都一样,Github 将自动执行修改后的 pipeline!! 这种情况甚至可能 更坏 如果 ”从 fork 向工作流发送写入令牌 pull requests“和”从 fork 向工作流发送机密和变量 pull requests” 已选中。除非有明确理由,否则不要这样做!!
如果“需要批准才能分叉 pull request 工作流程”被选中后,上述情况会有所增强:GitHub 将请求批准,并且不会执行修改后的 pipeline 对于读取用户,但它仍将为写入用户执行它。
看到叉子了,那 来自分支机构的 PR?
PR 来自 分支机构
为了保护这种情况,你必须依靠 分支保护规则.
在 repo 级别,您可以为任何分支创建分支保护规则。这些规则添加了一些 受保护分支修改的限制.
尽管您配置了一条规则来“需要 pull request 合并前“和”需要批准“ 修改后的 pipeline 创建 PR 时将自动执行此“批准”仅适用于合并行动。
间接中毒怎么办 Pipeline 执行
正如我们上面看到的,D-PPE 可以通过使用 拉取请求目标, 但它 不适用于 I-PPE.
如果您使用 pull_request_target,则默认签出将是基础代码。但是,如果您想验证对贡献代码(PR 代码)的某些检查,则需要明确签出 PR 代码。因此,如果 PR 代码修改了由 pipeline,“基础”(安全) pipeline 将调用“修改后的” shell 脚本 → 间接 PPE!!
这个问题的解决方案有点复杂(没有像 pull_request_target 这样的灵丹妙药)。
我们的 pipeline 由于我们使用了 pull_request_target,因此现在对 D-PPE 来说是安全的。但它仍然容易受到 I-PPE 的攻击。
在我们的测试示例中,我们需要检出 PR 代码才能进行构建,但测试是在构建生成的工件上执行的。
那么...... 为什么不检查两个代码库?
- 检查 PR 代码,因为贡献的代码是我们要构建和测试的
- 检查基础代码来运行原始版本 pipeline 以及构建/测试脚本
这可能通过 将这些代码库检出到不同的文件夹:基础代码可能会被签出到根文件夹,而 PR 则签出到其他文件夹。在这种情况下,我们将针对放置在新文件夹中的代码从根文件夹执行构建和测试脚本。
当然,这是一个简单的解决方案!但出于学习目的,我想介绍一个非常有趣的变体(…)
GitHub上 工作流运行 触发事件
除了 拉取请求目标除此之外,GitHub 还提供了另外一个触发事件: 工作流运行. 本次活动允许 执行 pipeline 适应另一个 pipeline的执行.
工作流运行 以及 拉取请求目标 触发器在一个方面是相似的:两者都将在特权模式下执行, 尽管 PR 进行了修改,但基础 pipeline 将被处决!!
让我们看看我们目前的 pipeline:
name: PR TARGET CI
on:
pull_request_target:
branches: [ main ]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
jobs:
prt_build_test_and_merge:
runs-on: ubuntu-latest
steps:
# checkout PR code
- name: Checkout repository
uses: actions/checkout@v4
with:
# This is to get the PR code instead of the repo code
ref: ${{ github.event.pull_request.head.sha }}
# Simulation of a compilation
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
ls -lR
# Simulation of running tests
- name: Running tests ...
id : run_tests
run: |
echo Running tests..
chmod +x runtests.sh
./runtests.sh
echo Tests executed.
#
# Let’s omit the check conditions at this moment …
#
- name: pr_check_conditions_to_merge
[...]
构建部分对于 D-PPE 来说是安全的,但是测试部分仍然容易受到 I-PPE 的攻击。
此 pipeline 本身对 D-PPE 来说是安全的,因为 拉取请求目标 触发器。但由于调用了外部 shell 脚本,测试步骤仍然容易受到 I-PPE 的攻击。
避免使用个人防护装备 (I-PPE)
上述目的 pipeline 是构建和测试贡献的代码,对 PPE 来说是安全的。
那么...... 为什么不分开 pipeline 分成两半? 一个用于构建,另一个用于测试..
- 1st pipeline (构建 CI) 会 检查 PR 代码(以构建它),进行构建并生成工件。
- 2nd pipeline (测试CI) 会 检查基本代码(避免修改 shell 脚本) 并针对工件执行原始脚本。
- 同步测试 CI pipeline 在 Build CI 之后运行 pipeline,我们将使用 工作流运行 触发。
这样:
- pipeline 构建 CI is 安全 二者皆是 聚苯醚 (由于 拉取请求目标) 以及 个人防护装备 (因为它不再执行shell脚本)。
- pipeline 测试CI 也是 安全 二者皆是 聚苯醚 (由于 工作流运行) 以及 个人防护装备 (因为它检出基础代码来获取原始的 shell 脚本)
让我们看看两者的代码 pipeline根据这些修改……
1 pipeline (构建CI):
name: Build CI
on:
pull_request_target:
branches: [ main ]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
GITHUB_PAT: ${{ secrets.GH_PAT }}
jobs:
prt_build_and_upload:
runs-on: ubuntu-latest
steps:
- name: Checking out PR code
uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request_target' }}
with:
# This is to get the PR code instead of the repo code
ref: ${{ github.event.pull_request.head.sha }}
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
# Save some PR info for later use by the 2nd pipeline
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
# Upload the binary as a pipeline artifact
- name: Archive building artifacts
uses: actions/upload-artifact@v3
with:
name: archive-bin
path: |
bin
2 pipeline (测试置信区间):
ame: Test CI
on:
workflow_run:
workflows: [ 'PR TARGET CI' ]
types: [completed]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
GITHUB_PAT: ${{ secrets.GH_PAT }}
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# By default, checks out base code (not PR code)
- name: Checkout repository
uses: actions/checkout@v4
# Download the artifact
- name: 'Download artifact'
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "archive-bin"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/myartifact.zip`, Buffer.from(download.data));
# Unzip the artifact
- name: 'Unzip artifact'
run: |
unzip -o myartifact.zip
# Runs tests
- name: Running tests ...
id : run_tests
run: |
echo Running tests..
chmod +x runtests.sh
./runtests.sh
echo Tests executed.
#
# Let’s omit the check conditions at this moment …
#
- name: pr_check_conditions_to_merge
[...]
哇… 不错的解决方案!!但是….. 我们安全吗?恐怕不行 😭
确实,我们引入了一个新的漏洞!哪一个?这将是我们下一篇文章的主题 🙂...敬请期待!
PS: 抱歉,我不能保持安静🤐..你听说过吗 神器中毒 ? 😂





