Building pull requests from forked repositories with GitHub Actions can be a bit tricky when it comes to secrets. As per the documentation, with the exception of GITHUB_TOKEN
, secrets are not passed to the runner when a workflow is triggered from a forked repository. This is to prevent the automatic execution of untrusted code that may be contained within the forked repo.
In other words, we can't use the pull_request
trigger if there are secrets that need to be involved in the workflow.
Fortunately, pull_request_target
comes to rescue.
...the
pull_request_target
event behaves in an almost identical way to the pull_request event with the same set of filters and payload. However, instead of running against the workflow and code from the merge commit, the event runs against the workflow and code from the base of the pull request. This means the workflow is running from a trusted source...
Ok, so now we have access to secrets but we're building the wrong code.
Apparently, there are some people who try to overcome this problem with the following code:
#INSECURE
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }} # Check out the code of the PR
This is highly discouraged and rightfully so, as it's insecure if no other security measures are taken.
In GitHub's own article Preventing pwn requests, the author - Jaroslav Lobačevski - suggests using the pull_request_target
in combination with a condition checking whether the PR is labeled safe to test
. Like this:
jobs:
build:
name: Build and test
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'safe to test')
This is a perfectly valid approach but I think I may have found a better and more convenient way of preventing unauthorized code execution during the build of forks.
Environment protection rules
Just a couple of months ago, GitHub introduced the Environment protection rules. The main intent of this feature is to protect environments during deployments by applying rules that will pause the execution of a workflow until given conditions are met - e.g. a human approval is given, the certain time elapsed, etc. But it can serve any general purpose. In our case, we'll use it to protect our repository secrets and to prevent the execution of untrusted code.
Protecting the build
Let's start with adding a dummy environment called "Integrate Pull Request" that will require human approval.
Our main build procedure will be associated with this environment and preceded by a dummy workflow step approve
that will kick off the workflow and inform the author of the pull request that a review is necessary before proceeding any further.
on:
pull_request_target:
branches: [ master ]
jobs:
approve: # First step
runs-on: ubuntu-latest
steps:
- name: Approve
run: echo For security reasons, all pull requests need to be approved first before running any automated CI.
build: # Second step
runs-on: ubuntu-latest
needs: [approve] # Require the first step to finish
environment:
name: Integrate Pull Request # Our dummy environment
steps:
- ...
This way the workflow won't proceed until someone reviews the submitted code and therefore, we can safely check out the ${{ github.event.pull_request.head.sha }}
in the next step and build it. So the build is executed using a trusted workflow from the base of the PR and the actual code of the PR.
How it works in practice
Someone submits a pull request and a workflow is triggered and immediately paused
The reviewer or a group of reviewers receive an e-mail notification
The reviewer clicks the link, navigates to the repo, verifies that the submitted PR doesn't contain any unwanted code, and finally gives an approval
A few words on Codecov
While implementing this workflow, I ran into an issue where the Codecov action, similarly to the GitHub Checkout Action, is by default pointed to the PR's Base and needs to be overridden. This can be achieved by:
- name: Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
override_pr: ${{ github.event.number }}
override_commit: ${{ github.event.pull_request.head.sha }}
To get rid of the following warning message:
Issue detecting commit SHA. Please run actions/checkout with fetch-depth > 1 or set to 0
make sure to also set fetch-depth
of the Checkout action to 2.
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
Note: I found an alternative approach using a conditional workflow step and a shell script. But overriding the commit SHA is far easier.
Summary
The advantage of this approach is that you can assign a group of reviewers who'll receive an email notification about the pending workflow and can review and approve it in a single click.
The process is, in my opinion, more transparent thanks to all events being logged and semantically more correct than using labels.
If you want to explore the whole workflow, feel free to check out my project WopiHost.
To learn more about the specifics of pull_request_target
head to the documentation.
Top comments (2)
You're a life saver! Just one question, is there a way to only need approval when the contributor is outside of an organization?
Thank you so much for doing this 🙏
Hey @zomars , I totally missed your comment. It seems that GitHub now has a way to configure this: