Intro: Maintainers Caught at a Fork 🍴
How can maintainers safely test forked PRs without exposing sensitive data to potentially untrusted code? This article explores how on forked pull requests authored by public code contributors, with no elevated permissions from repository / organization roles, can be securely merged.
Context
Terminology:
- Event Trigger: Event that initiates a workflow in GitHub Actions (like push, pull_request, and workflow_run).
- Context (in GitHub Actions): The environment of where the workflow run, determined by the event trigger, repository, and branch.
- actions/checkout: A GitHub Action that checks out a repository at a specific commit or ref so a workflow can access the code in the workflow's context.
- pull_request: An event trigger that runs your workflow when activity on a pull request in the workflow's repository occurs.
- Head (source): refers to the branch / repository containing the proposed code changes.
- Base (target): refers to the branch / repository where proposed code changes are being merged into.
Workflow Permissions - Internal vs. Forked Pull Requests:
When a workflow includes the actions/checkout action, it uses the GITHUB_TOKEN to authenticate and fetch the repository at a specific commit or ref, placing the files into the $GITHUB_WORKSPACE directory, the workflow's context, so subsequent steps can access them. By default, workflows triggered by pull_request run actions/checkout in the context of the pull request’s head commit. But for forked pull requests, the GITHUB_TOKEN is read-only and secrets are unavailable. GitHub intentionally configures this as a security measure.
This design ensures untrusted code does not access repository secrets, meaning secret-dependent workflows fail on forks.
Safety:
Risks of Granting Secrets Access on Untrusted Code:
While security mitigations minimize risk, there is no foolproof way to securely allow secrets access on untrusted forked PRs. From injecting malicious code to exploiting race conditions, I invite readers to view this 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
So here are 3 solutions, ranging from least to most secure, for running secret-dependent workflows, such as tests, on untrusted forks.
Solutions:
Solution 1 - Dangerous Use of pull_request_target:
Introduced in 2021, GitHub provides the pull_request_target event to elevate the GITHUB_TOKEN permissions to read/write on forked pull requests. By default, this sets the context as the base repository, rather than the fork, allowing metadata-only (no secrets) workflows to run, such as automated labels, comments, project board updates, etc. within a trusted context.
Unlike pull_request, secrets-dependent workflows run successfully on forks, but not in the context of the proposed code changes; misleading unaware developers with false test results. To run secrets-dependent workflows on proposed code changes in untrusted forks, the fork’s PR head SHA must be explicitly set with actions/checkout:
name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
But this is dangerous on it's own. The primary use case for pull_request_target is non-invasive, maintainer tasks on forks, not running or building untrusted code. Any developer using pull_request_target for any reason should carefully read the warnings and precautions here: GitHub Docs: pull_request_target.
Solution 2 - The workflow_run Prescan:
Also introduced in 2021, the workflow_run event enables chained workflows and 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
The trick is to create 2 workflows; first for static analysis of the untrusted code, the second for granting secrets access. This helps minimize the risk of exposing secrets to untrusted code by isolating read and write permission jobs.
The prescan is triggered by pull_request, setting the untrusted code in it's context and no secrets access. Upon successful completion of the prescan, the second workflow is triggered by workflow_run, granting secrets access to perform secret-dependent workflow runs on proposed code changes in forks, such as tests.
The prescan workflow should fail if:
- Author is verified as untrusted with
ifcomparisons. For example, if the head (source) respository's full name (owner-name/repo-name and always unique) matches trusted authors or by author permissions. - Changes made to sensitive files like
.github/workflows/*,.gitignore, scripts. - Fail on static analysis via CodeQL.
Solution 3 - Use Isolated, Disposable Environments:
In my opinion, this is the best solution is simply running tests locally by cloning into an isolated, disposable environment after successful completion of a prescan workflow.
Best Practices:
- Require contributors to obtain their own test secrets (if accessible), run tests locally, and share screenshot of results in the PR template. Clearly document in README and CONTRIBUTING file. This does not negate the need to run your own tests.
- Always require maintainer approval before workflow runs on forks.
- Never expose secrets directly in a
pull_request_targetworkflow checking out PR code. Use dummy secrets with limited permissions and monitor them regularly. - Never use variable untrusted data in prescan workflow logic, such as
github.event.pull_request.titlewhich users can alter during workflow runs. - Prevent script injection in run steps and prefer GitHub Actions over inline scripts.
- Pin marketplace Actions versions with commit SHAs.
- Stay updated on security hardening for GitHub Actions.
Further Reading:
- GitHub Blog: Actions Improvements for Fork and Pull Request Workflows
- GitHub Blog: Four Tips to Keep You GitHub Actions Workflows Secure
- GitHub Blog: How to Secure Your GitHub Actions Workflows with CodeQL
- GitHub Blog: Security Best Practices for Authors of GitHub Actions
- GitHub Docs: Actions Security Hardening for GitHub Actions
- GitHub Docs: Pull Request Events for Forked Repositories
- GitHub Docs: Workflows in Forked Repositories
- GitHub Security Lab: Preventing Pwn Requests
- GitHub Well-Architected Docs: Securing GitHub Actions Workflows Guide
- Michael Heap Blog: How to Safely Access Secrets from Forks
Top comments (0)