poisoned-pipeline-execution-II

深入探討 CI/CD Pipelines Vulnerabilities (II) : Indirect Poisoned Pipeline 執行(I-PPE)

In our previous post, we saw how to detect and protect against Direct Poisoned Pipeline Execution (D-PPE). We also saw how to detect that vulnerability using Xygeni Scanner, as well as some protection mechanisms. 

 中毒 Pipeline 執行 (PPE) is produced when the attacker can modify the pipeline logic in either of two ways:

  • By modifying the CI config file (the pipeline)-> 直接個人防護裝備(D-PPE)
  • By modifying files referenced by the pipeline (for example: scripts referenced from within the pipeline configuration file) -> 間接個人防護裝備(I-PPE)
pp2

In this post, we will deep dive into Indirect PPE . But, before that, and as a complement to my previous post, let’s see first how GitHub manages the execution of pipelines and what are the protection mechanisms against D-PPE.

How does GitHub protect the execution of pipelines coming from PRs?

How does GitHub work regarding the execution of modified pipelines?

修改時間 pipelines can come from Pushes or Pull Requests (PR)。 As a major best practice, it’s strongly recommended to avoid any direct “push” to a protected branch and use Pull Requests as a mechanism to enforce some review before accepting any contributed code. 

Pull Requests may arrive from two different sources:

  • PRs coming from 叉子
  • PRs coming from 分館

PRs from 叉子 can come either from 公眾 or 私立 倉庫。

As we are dealing with PPE (Poisoned Pipeline Execution), our main point is not the “acceptance” of a PR but the execution of a modified pipeline during the PR’s acceptance/approval process. At the core of a PPE attack, there is an unintended execution of a  “malicious” modified pipeline. 

In a few words, Poisoned Pipeline Execution (PPE) is produced when the attacker can modify the pipeline 邏輯.

那裡有兩個 變種:

  • Direct PPE (D-PPE): In a D-PPE scenario, the attacker modifies the CI config file in a repository they have access to, either by pushing the change directly to an unprotected remote branch on the repo, or by submitting a PR with the change from a branch or a fork. Since the CI pipeline execution is defined by the commands in the modified CI configuration file, the attacker’s malicious commands ultimately run in the build node once the build pipeline 被觸發。
  • Indirect PPE (個人防護裝備): In certain cases, the possibility of D-PPE is not available to an adversary with access to an SCM repository (e.g. if the pipeline is configured to pull the CI configuration file from a separate, protected branch in the same repository). In such a scenario, rather than poisoning the pipeline itself, an attacker injects malicious code into files referenced by the pipeline (for example: scripts referenced from within the pipeline configuration file)

在這兩種情況下 GitHub will execute the modified pipeline with no need for a previous review or approbation.

PRs from forks on 公眾 回購協議

GitHub allows configuring the behaviour when processing PRs coming from forks in public repos.

When a PR is coming from a fork, GitHub always forces some level of “approval” before executing the pipeline associated with the PR. This level of approval trades off from a weak to a strict approval.

At Org level (Org>>Settings>>Actions>>General), you can decide among several “approval” options:

ppe3

The strictest is the last one (“Require approval from all outside collaborators”) because GitHub will always require approval when the PR is coming from forks from outside collaborators. 

But even in this strict case, there are differences between collaborators with read and write permissions.

  • When the PR comes from a 閱讀 用戶, 的執行 pipeline is STOPPED until there is an approval of changes. If the approval is ok, then the modified pipeline 被執行。 
  • When the PR comes from a 用戶, approval is not needed and the modified pipeline is always executed !! 
pp4

As a conclusion, PRs coming from forks on public repositories are lightly protected against PPE. There is some protection against external (read) users, but nothing related to internal (write) users.

關於什麼 PRs coming from forks from private repos?

PRs from forks on 私立 回購協議

In this scenario, GitHub provides some useful configuration settings.

ppe9

Above settings can be configured either at 組織 回購 水平。

日期 no option is checked, GitHub will ask for approval 以及 it will not execute the modified pipeline. This is the safest configuration!!

unsafest configuration“不"Run workflows from fork pull request” is checked. In this case, same for both read and write users, Github will automatically execute the modified pipeline!! And this situation can be even 更糟糕 如果 ”Send write tokens to workflows from fork pull requests“和”Send secrets and variables to workflows from fork pull requests” are checked. Do not do this unless clearly justified!!

如果“Require approval for fork pull request 工作流程” is checked, the above situation is somewhat enhanced: GitHub will ask for approval and not execute the modified pipeline for the read user, but it will still execute it for a write user.

ppe6

Forks seen, what about PRs coming from branches?

PRs from 分館

To protect this scenario you must rely on 分支保護規則

At repo level, you can create branch protection rules for any branch. These rules add some constraints to modification of protected branches.

Although you configure a rule to “需要 pull request 合併之前“和”Require approvals修改後的 pipeline will be automatically executed upon PR creation.The “approval” will only apply to the merge action.

ppe7

What about Indirect Poisoned Pipeline 執行

As we saw above, D-PPE can be mitigated by using pull_request_target,但它 does not apply to I-PPE.

If you use pull_request_target, the default checkout will be the base code. But if you want to validate some checks on the contributed code (PR code) you need to explicitly checkout the PR code. Therefore, if the PR code has modified any shell script invoked by the pipeline, the “base” (safe) pipeline will invoke the “modified” shell script → Indirect PPE!!

The solution to this is a bit more complicated (there is not a magic bullet like pull_request_target). 

我們的 pipeline is now safe to D-PPE because we are using pull_request_target. But it is still vulnerable to I-PPE. 

In our test example, we need to checkout the PR code basically to make the build, but the tests are executed on the artifact generated by the build. 

所以.. why don’t check out both codebases? 

  • Checkout PR code because is the contributed code what we want to build and test
  • Checkout Base code to run the original version of the pipeline and the build/tests scripts 

This might be done by checking out those codebases to different folders: the base code might be checked out to the root folder, and the PR to a different folder. In this case we would execute the build and the test script from the root folder against the code placed into the new folder.

This is an easy solution, of course!! But, for learning purposes I would like to introduce a quite interesting variant (…) 

GitHub上 工作流程運行 觸發事件

除了 pull_request_target, GitHub provides another trigger event: 工作流程運行. This event allows execution of a pipeline conditioned to another pipeline’s execution

工作流程運行 以及 pull_request_target triggers are similar in one aspect : both will be executed in privileged mode and, despite the PR modifications, the base pipeline will be executed !! 

Let’s see our current 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         [...]

The build section is safe to D-PPE, but the test section is still vulnerable to I-PPE.

这 pipeline itself is safe to D-PPE due to the pull_request_target trigger. But the test step is still vulnerable to I-PPE due to invoking an external shell script.

Avoiding I-PPE 

The purpose of the above pipeline is to build and test the contributed code, being safe to PPE. 

所以.. Why don’t split the pipeline into two ? One for building and another for testing..

  • 該1st pipeline (建置 CI) 會 查看 PR 程式碼(以進行建置)進行建置並產生工件。
  • 2nd pipeline (測試 CI) 會 檢出基礎程式碼(以避免修改 shell 腳本) 並對工件執行原始腳本。 
  • 為了同步測試 CI pipeline 在建置 CI 之後運行 pipeline我們將使用 工作流程運行 觸發。 
ppe8

In this way:

  • pipeline 建置 CI is 安全至上 二者皆是 D-PPE (由於 pull_request_target) and 個人防護裝備 (因為它不再執行 shell 腳本)。
  • pipeline 測試 CI安全至上 二者皆是 D-PPE (由於 工作流程運行) and 個人防護裝備 (因為它會檢出基礎程式碼以取得原始 shell 腳本) 

Let’s see the code of both pipelines according to these modifications …

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 (測試 CI):

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         [...] 

Wow… nice solution!! But ….. Are we safe? I’m afraid that no 😭

Indeed, we have introduced a new vulnerability!! Which one?  This will be the subject of our next post  🙂 … Stay tuned!! 

注: Sorry, I can’t keep quiet 🤐 ..Have you heard about 神器中毒 ? 😂

Artifact Poisoning and Code Injection​​

深入探討 CI/CD Pipeline漏洞(三)

透過軟體證明防止工件中毒

深入探討 CI/CD Pipeline漏洞(IV)

中毒 Pipeline 執行(PPE)

深入探討 CI/CD Pipeline漏洞(I)
sca-tools-software-composition-analysis-tools
優先處理、補救並保護您的軟體風險
註冊免費帳號。
不需要信用卡。

確保您的軟體開發和交付安全

使用 Xygeni 產品套件