How To Use pull_request_target: Access Secrets and Checkout Code on Forked Pull Requests
Maintainers Caught at a Fork 🍴
The Problem:
As an open-source maintainer, I am responsible for reviewing contributions from the public developer community before merging them into the production codebase. A critical step in this process involves running tests on forked pull requests (PRs), especially for end-to-end (e2e) integration tests. However, these tests often require access to repository secrets, such as test tokens, which raises a fundamental question: How can I safely test forked PRs without exposing sensitive data to potentially untrusted code?
Background:
Terminology:
A Cybersecurity Problem Emerges:
To run tests or execute workflows on a forked PR, the actions/checkout action is typically employed. This checks out the repository code under $GITHUB_WORKSPACE and fetches the commit associated with the PR. This is required when you need to access the forked pull request’s code such as for running tests, analytics, building or deploying to staging, executing scripts, modifying files, or generating artifacts. If the workflow only handles metadata (ie. labeling PRs, posting comments), the code does not need to be checked out, which can save execution time.
In cases where e2e integration tests are required, granting a forked PR access to GitHub secrets is often necessary. While security mitigations can minimize risk, there is no foolproof way to allow access to secrets in public, forked PRs without potential exposure. The risk is particularly concerning when an attacker injects malicious code that could exploit vulnerabilities like the infamous Time of Check to Time of Use (TOCTOU). For a deeper dive into such vulnerabilities, see this surprisingly horrifying repository for a list of all the things that can go wrong. Ultimately, all public code contributions need to be treated as untrusted code:
“Any automated processing of PRs from an external fork is potentially dangerous and such PRs should be treated like untrusted input.” - Jaroslav Lobačevski, GitHub, writes in Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests
Why the Default Event Trigger Falls Short:
For security reasons, the default pull_request event trigger restricts access to secrets on forked PRs. GitHub intentionally configures this as a security measure — all workflows triggered by pull requests from a fork, only have read-only permissions with no access to secrets. To run workflows with access to secrets on forked pull requests, you will need the pull_request_target event trigger. pull_request_targetsafely grants access to secrets by running in the context of the base repository, not in the forked pull request’s repository. However, using pull_request_target requires careful consideration. This trigger does not check out the PR's code by default, meaning it cannot directly run tests on the proposed changes. If you use the wrong event trigger in the incorrect context, this can lead misleading test results.
Here is a breakdown of how each event trigger behaves:
It’s Not Too Late to Turn Back:
So you still want to grant secrets access on forked pull requests? While technically feasible, it is not a standard practice in the open-source community due to the inherent risks. If your repository handles sensitive data, you should carefully weigh the potential consequences and proceed with caution.
Instead, consider alternative approaches before enabling secrets access on forked PRs:
Still want to grant access to secrets on untrusted forked pull requests? Here’s how you can implement it as securely as possible:
The Pull Request Target Trigger: A Deep Dive
Introduced in 2021, the pull_request_target event trigger behaves similarly as pull_request, except runs workflows in the context of the base branch (ie. main), ensuring access to secrets without directly executing code from the forked PR. Note that GitHub clearly warns against using this trigger for tasks like running tests on untrusted code, as it bypasses the usual safety checks. The security risks are well-documented, thus this warning message is shown in GitHub official docs:
For workflows that are triggered by the pull_request_target event, the GITHUB_TOKEN is granted read/write repository permission unless the permissions key is specified and the workflow can access secrets, even when it is triggered from a fork. Although the workflow runs in the context of the base of the pull request, you should make sure that you do not check out, build, or run untrusted code from the pull request with this event. Additionally, any caches share the same scope as the base branch. To help prevent cache poisoning, you should not save the cache if there is a possibility that the cache contents were altered. - GitHub Docs: pull_request_target (as of Feb 2025).
The primary use case for pull_request_target is for non-invasive tasks, such as applying labels, enforcing repository policies, or commenting on PRs. Using it to run build or test workflows on forked PRs can expose your repository to risk:
Recommended by LinkedIn
We’ve added a new pull_request_target event, which 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 and is given access to a read/write token as well as secrets enabling the maintainer to safely comment on or label a pull request. - GitHub Changelog: GitHub Actions improvements for fork and pull request workflows (2021).
If you already decided to risk it all and proceed, here's how to set up a workflow that properly checks out a forked PR's code:
Pull Request Target Workflow with Code Checkout:
There are many online developer forums repeating the pull_request_target method while failing the mention this vital step. To run tests on forked PR code, you must explicitly check out the head REF of the pull request’s branch. The default context of pull_request_target runs against the base branch and does not fetch the code from the forked PR unless explicitly instructed to do so. Here is an example of proper use of pull_request_target with an explicit checkout of the forked PRs code:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
Understanding the workflow_run Event Trigger:
Also introduced in 2021 along with pull_request_target, the workflow_run event trigger enables chained workflows. It allows a subsequent workflow to run only if a preceding workflow completes successfully. Additionally, it elevates permissions:
“The workflow started by the workflow_run event is able to access secrets and write tokens, even if the previous workflow was not.” - GitHub Official Documentation
Example workflow to prevent secrets from being exposed to failing forked PRs:
on:
workflow_run:
workflows: [Prescan]
types: [completed]
jobs:
on-success:
runs-on: <your_runner>
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- run: echo 'The triggering workflow passed'
on-failure:
runs-on: <your_runner>
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- run: echo 'The triggering workflow failed'
Recommended Security Strategies:
To mitigate the risks of granting secrets access on forked PRs, consider the following two-step workflow process:
The separating these workflows helps minimize the risk of exposing secrets to untrusted code by isolating read and write permission jobs.
Pre-scan Workflow Tips:
The pre-scan workflow should perform the following checks to enhance security:
GitHub Actions Security Best Practices:
When you are granting secrets access to untrusted code, it is especially important to apply the best security practices for GitHub Actions:
Further Reading:
This is great!