Introduction
Hey everyone, my name is @iso and I’m working on the Platform Security Team at Mercari.
One of the major functions of our team is to ensure the security of Mercari’s GitHub code repositories with many different areas to consider in achieving this.
In this post, we’ll take a look at branch protection (protected branches) on GitHub; in particular, whether it’s possible for attackers to bypass rules requiring approval to merge pull requests. If you want to keep your branches safe, keep reading!
How we use GitHub at Mercari
Mercari uses GitHub to manage code. This includes not only app and backend code, but all sorts of files related to infrastructure, like files used for Terraform and Kubernetes. The data stored on GitHub plays a crucial role in our development process.
Different organizations may have different policies for GitHub permissions, but at Mercari, developers generally have write permissions for many repositories, including repositories used by other teams. (Of course, due to the nature of the content of some repositories, they are only accessible to a limited number of developers.) This means that developers can create new branches and pull requests (PRs) on other teams’ repositories or make pull requests that affect infrastructure in repositories that contain Terraform- or Kubernetes-related files.
While it’s convenient for developers to have write permissions for many different repositories, it’s not good if developers who have no affiliation with a certain repository can arbitrarily overwrite the code or modify important Terraform files without any form of review. That’s where branch protection rules and branch rulesets come in—with these rules, you can add a layer of security by requiring pull request reviews and approval before any changes can be merged into the default branch (main/master branch). At Mercari, we enforce branch protections for all repositories involved in production.
(Technically, branch protection rules and branch rulesets as used on GitHub have some differences, but for the purposes of this post, they’re functionally the same, so I’ll use the term "branch protection" to collectively refer to both.)
Methods attackers may use to get around branch protection
So now that we’ve established that branch protection plays a crucial role in protecting your repositories, what’s the best configuration to use? Can branch protection really protect your repositories from all types of attacks? Let’s find out!
Assumptions
Let’s assume the following simple conditions:
- Situation: All developers that can access the repository have write permissions
- Requirement: Changes to the main branch must be approved by at least one other person (= no developer can modify the main branch by themselves)
- In order to fulfill this requirement, let’s assume that the repository uses the branch protection rule "Required number of approvals before merging: 1"
Cast
To help us visualize each attack method, I’ll be walking through them using two characters.
![]() |
|
Alice | A software engineer. Alice writes and reviews code on a daily basis. She has a keen sense of smell that can sniff out malicious code in code reviews, no matter how cleverly hidden it may be. |
![]() |
|
Mallory | An attacker. Mallory has big ambitions. She somehow acquired write permissions to a repository and is attempting to insert a backdoor in the code on the main branch. |
The roles involved in a pull request
Before we get into the attack methods, let’s lay out how pull requests work and the different roles involved.
Pull requests are created by users or bots. I’ll refer to this person (or bot) as the "PR creator."
"Last commit pusher" refers to the user who pushed the most recent commit to the source branch (the merge base) of the pull request. In many cases, the PR creator is the last commit pusher ("PR creator" == "last commit pusher"
), but this is not always the case.
Under the conditions we defined earlier in our assumptions, a pull request must be approved by at least one person. Let’s call this user the "PR approver." The person who created the pull request can’t approve it themselves, so we can say that in all cases, it holds true that the PR creator is not the PR approver ("PR creator" != "PR approver"
).
After a pull request is approved, it is merged into the main branch, but anyone with write permissions to the repository can merge the pull request. For the purposes of this post, it doesn’t matter who this person is.
Attack pattern 0: Mallory creates a pull request, and Alice reviews it
First, let’s think about the simplest attack method: Mallory creates a pull request that includes malicious code, and Alice reviews it.
As mentioned earlier, Alice’s keen sense of smell enables her to sniff out all malicious code in pull request reviews, so she finds the malicious code, rejects the pull request, and thwarts Mallory’s attack. This enables us to rule out all attack patterns in which Alice would be the PR approver.
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Mallory | Mallory |
Attack pattern 1: Mallory pushes a commit to a pull request Alice has created and approves the pull request (pull request hijacking)
This method is known as pull request hijacking. You can read more about it in this article:
https://www.legitsecurity.com/blog/bypassing-github-required-reviewers-to-submit-malicious-code
Pull requests can be approved by anyone (other than the PR creator) who has write permissions to the repository. This means that a malicious user could commit an arbitrary change to another person’s pull request, then approve and merge it themselves.
Alice may notice if a pull request she created has a commit added and is merged into the main branch, but if the pull request is created by a bot like Dependabot, it’s possible that no one will notice.
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Alice | Mallory | Mallory |
This attack method can be prevented by enabling the "Require approval of the most recent reviewable push" setting. Enabling this setting adds an additional rule requiring that the last commit pusher is not the PR approver ("last commit pusher" != "PR approver"
) meaning that Mallory won’t be able to approve the pull request.
Attack pattern 2: Mallory creates a pull request and uses GitHub Actions to approve it
In some repository configurations, a GITHUB_TOKEN automatically generated in a GitHub Actions workflow may be used to approve a pull request. Anyone with write permissions to the repository can create or add to a GitHub Actions workflow, so Mallory would be able to create a workflow to approve the pull request that she made.
When using a GITHUB_TOKEN to approve a pull request, the PR approver becomes "github-actions." This is treated as a separate user from Mallory.
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Mallory | Mallory | github-actions |
This attack method can be prevented by disabling the "Allow GitHub Actions to create and approve pull requests" setting. Disabling this setting adds an additional rule requiring that neither the pull request creator nor the pull request approver are github-actions ("PR creator" != github-actions && "PR approver" != github-actions
).
Attack pattern 3: Mallory creates a pull request using GitHub Actions and approves it
In this attack pattern, similar to pattern 2, Mallory uses a GitHub Actions workflow to create a pull request and add code, and then approves the pull request herself.
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
github-actions | github-actions | Mallory |
This attack method can be prevented the same way as attack pattern 2: by disabling the "Allow GitHub Actions to create and approve pull requests" setting.
Summary so far
Let’s summarize the attack patterns we’ve described so far, as well as other possible patterns.
In the table below, countermeasure 1 and countermeasure 2 are defined as follows:
- Countermeasure 1: Enable the "Require approval of the most recent reviewable push" setting
- Countermeasure 2: Disable the "Allow GitHub Actions to create and approve pull requests" setting
Attack Pattern | PR Creator | Last Commit Pusher | PR Approver | Can this be prevented with countermeasure 1? | Can this be prevented with countermeasure 2? |
---|---|---|---|---|---|
1 | Alice | Mallory | Mallory | ✅ Yes | ❌ No |
2 | Mallory | Mallory | github-actions | ❌ No | ✅ Yes |
3 | github-actions | github-actions | Mallory | ❌ No | ✅ Yes |
4 | github-actions | Mallory | Mallory | ✅ Yes | ✅ Yes |
5 | Mallory | github-actions | github-actions | ✅ Yes | ✅ Yes |
6 | Alice | Mallory | github-actions | ❌ No | ✅ Yes |
7 | Alice | github-actions | Mallory | ❌ No | ❌ No |
Attack pattern 7: Mallory adds a commit to a pull request Alice has created using GitHub Actions and approves it herself
Attack patterns 1–6 can be prevented by changing the settings on GitHub. However, unless we change the assumed conditions, there doesn’t appear to be a way to prevent attack pattern 7.
In this pattern, Mallory uses GitHub Actions to add malicious code to a pull request created by Alice. Mallory then approves and merges the pull request herself. (The pull request that Mallory adds code to using GitHub Actions doesn’t need to be a pull request created by Alice. It could be a pull request created by a bot like Dependabot or an open pull request that has been long forgotten. In either of these cases, it’s unlikely anyone would notice the attack.)
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Alice | github-actions | Mallory |
How to prevent attack pattern 7
In this attack pattern, the PR creator, last commit pusher, and PR approver are all different users, enabling Mallory to bypass the settings we’ve discussed so far.
The method GitHub offers to prevent this attack is to set the required number of approvals before merging to 2 or more. However, increasing this number lowers developer productivity and is not a great solution.
Enabling the "Require review from Code Owners" setting can make it harder for an attacker to use this attack pattern, but if Mallory is a code owner, she can always bypass the setting. This setting may lower the success rate of attacks, but it can’t prevent them entirely.
Currently, it isn’t possible to prevent this attack using just features provided by GitHub, so in order to close off this attack pattern, it’s necessary to develop some sort of mechanism yourself. Some possible examples:
- Create a mechanism that raises an alert when a pull request that looks like the one in attack pattern 7 is merged
- Set the required number of approvals before merging to 2 and have a bot approve the pull request if it doesn’t look like the one in attack pattern 7; this will enable a pull request to be merged with approval from one person and a bot
Here, I should note that I notified GitHub about the lack of features that would prevent this attack pattern in May 2024. GitHub responded saying that this is expected behavior. They also gave permission for me to publish this blog post.
Conclusion
In this post, we covered branch protection on GitHub, methods an attacker might use to evade branch protection, and countermeasures that can be taken to prevent those attack methods. Branch protection is a powerful feature that can be used to protect important branches, but it isn’t perfect; under the right conditions, it can be bypassed using GitHub Actions. I hope this information helps readers use GitHub more securely in both their personal and work repositories.