はじめに
こんにちは、Platform Securityのisoです。この記事は、Mercari Advent Calendar 2024の記事です。
本記事ではGitHubのbranch protection(protected branch)について、特にpull requestのマージに承認が必要とする制約をどうにかして突破できないかについて考察します。ぜひ最後までお読みいただけると嬉しいです。
メルカリにおけるGitHub
メルカリではGitHubを使ってコードの管理をしています。アプリやバックエンドのコードだけではなく、TerraformやKubernetesなどインフラに関わるあらゆるファイルをGitHubを使って管理しておりGitHub上のデータは非常に重要な役割を担っています。
組織によって開発者に付与するGitHubの権限は様々だと思いますが、メルカリの開発者は基本的に(自チーム以外のリポジトリを含む)多くのリポジトリに書き込み権限を持っています。(もちろんリポジトリの内容を考慮し、限られた開発者のみがアクセスできるリポジトリもあります。)これにより他チームのリポジトリに新しくブランチを作成してpull request(以降、PR)を作成したり、TerraformやKubernetes関連のファイルが保存されているリポジトリにPRを作成してインフラを構成したりといったことが可能となっています。
色々なリポジトリに書き込み権限を持っていることは便利な一方で、そのリポジトリとは全く関係のない開発者がコードを勝手に書き換えられたり、重要なTerraformのファイルをレビューなしで変更できたりしてしまうのは好ましくありません。そこでbranch protection ruleあるいはbranch rulesetを使うことで、デフォルトブランチ(main/masterブランチ)への変更はPRの作成を必須化し、マージには承認を必要とするというセキュアな運用を実現できます。メルカリでは、プロダクションに関わるリポジトリにはすべてこの設定を導入しています。
(なお、GitHubにおいてブランチを保護する方法としてbranch protection ruleとbranch rulesetがありますが本記事が扱う内容においては2つに違いはないため、特に区別せずにbranch protectionと呼びます。)
Branch Protectionへの攻撃方法
さて、このようにbranch protectionはリポジトリを守る上で重要な役割を担うわけですが、どのような設定をしたら良いのでしょうか。また本当にbranch protectionで大切なブランチを守り切れるのでしょうか。
前提条件
以下のシンプルな条件で考えてみます。
- 前提: リポジトリにアクセスできるすべての開発者がリポジトリに書き込み権限を持っている
- 要件: mainブランチの変更は最低1人からの承認を必須とする(mainブランチは1人では変更できてはいけない)
- これを満たすためにbranch protection ruleにおいて"Required number of approvals before merging: 1"が設定されているものとする
登場人物
攻撃方法を検討する上で2人の人物に登場してもらいます。
Alice | ソフトウェアエンジニア。日々、コードを書いたりレビューしたりしている。レビューの際にはどんなに巧妙に隠された悪意あるコードも見つけ出すことができる鋭い嗅覚の持ち主。 |
Mallory | 攻撃者。大きな野望を実現するため、とある方法でリポジトリへの書き込み権限を入手し、mainブランチのコードにバックドアを仕掛けようとしている。 |
Pull requestの役割の整理
実際の攻撃方法を考える前に、PRにおける役割を整理します。
PRはユーザーやbotなどによって作成されます。本記事ではPRの作成者を"PR creator"と呼びます。
PRのソースブランチ(マージ元)に最後にコミットをプッシュしたユーザーを"last commit pusher"と呼びます。多くの場合で"PR creator" == "last commit pusher"
ですが必ずしもそうである必要はありません。
今回の条件下ではPRは最低1人から承認されている必要があります。PRを承認したユーザーを"PR approver"と呼びます。PRの作成者は自身が作成したPRを承認できないので"PR creator" != "PR approver"
が常に成り立ちます。
PRは承認された後にマージされますが、マージはリポジトリに書き込み権限があれば誰でもでき、今回の攻撃方法の考察には関わってきません。
攻撃パターン0: MalloryがPRを作成しAliceにレビューしてもらう
まずは最もシンプルにMalloryが悪意あるコードを含むPRを作成しAliceにレビューしてもらうことを考えてみます。
前述の通り、AliceはPRに含まれる悪意あるコードを持ち前の嗅覚で必ず見つけ出すのでこのPRは承認されず、攻撃は失敗に終わります。つまり、今回攻撃パターンを考える上ではAliceがPR approverとなるパターンは検討する必要がありません。
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Mallory | Mallory |
攻撃パターン1: Aliceが作成したPRにMalloryがコミットをプッシュし承認する(PR Hijacking)
この攻撃方法は次の記事で紹介されているPR hijackingと呼ばれる方法です。
https://www.legitsecurity.com/blog/bypassing-github-required-reviewers-to-submit-malicious-code
PRは"PR opener"以外のリポジトリに書き込み権限があるユーザーなら誰でも承認ができるため、誰かが作ったPRに勝手にコミットを追加し、承認してマージすることが可能です。
Aliceは自分が作成したPRに勝手にコミットが追加され、マージされたことに気づく可能性はありますが、このPRがDependabotのようなbotによって作成されていた場合、このことに誰も気付けない可能性があります。
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Alice | Mallory | Mallory |
この攻撃は"Require approval of the most recent reviewable push"というオプションを有効化することで防ぐことができます。このオプションを有効化すると"last commit pusher" != "PR approver"という制約を追加することができるのでMalloryはPRを承認できなくなります。
攻撃パターン2: MalloryがPRを作成しGitHub Actionsで承認する
リポジトリの設定によっては、GitHub Actionsのワークフローで自動生成されるGITHUB_TOKENを使ってPRを承認することができます。GitHub Actionsのワークフローはリポジトリの書き込み権限があれば誰でも作成・追加できるため、Malloryが自身が作成したPRを承認するようなワークフローを作成することも可能です。
GITHUB_TOKENを使ってPRを承認した場合、承認したユーザーは"github-actions"となりMalloryとは別のユーザーがPRを承認したものとして扱われます。
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Mallory | Mallory | github-actions |
この攻撃方法はAllow GitHub Actions to create and approve pull requestsを無効化することで防ぐことができます。このオプションを無効化すると"PR creator" != github-actions && "PR approver" != github-actionsという制約を加えることができます。
攻撃パターン3: GitHub ActionsでPRを作成しMalloryが承認する
攻撃パターン2の応用として、MalloryがGitHub ActionsのワークフローでPRの作成とコードの追加を行い、Mallory自身がPRを承認するという方法もあります。
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
github-actions | github-actions | Mallory |
この攻撃方法も攻撃パターン2と同様にAllow GitHub Actions to create and approve pull requestsを無効化することで防ぐことができます。
ここまでのまとめ
これまで紹介した攻撃パターンとその他に考えうる攻撃パターンを表にまとめます。
なお、表内の対策1と対策2はそれぞれ次に対応します。
- 対策1: "Require approval of the most recent reviewable push"の有効化
- 対策2: "Allow GitHub Actions to create and approve pull requests"の無効化
Attack Pattern | PR Creator | Last Commit Pusher | PR Approver | 対策1で防げるか | 対策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 |
攻撃パターン7: Aliceが作成したPRにMalloryがGitHub Actionsでコミットを追加し、Mallory自身が承認する
表に記載の攻撃パターン1-6はGitHubのオプションを変更することで防ぐことができます。しかし、攻撃パターン7を防ぐ方法は(前提条件を変更しない限り)なさそうです。
具体的にはAliceが作成したPRにMalloryがGitHub Actionsを使って悪意あるコードを追加します。そしてMallory自身がPRを承認しマージします。(コードを追加するPRは必ずしもAliceが作成したPRである必要はなく、Dependabotのようなbotが作成したPRやオープンのまま忘れ去られているPRなどでも問題ありません。このようなPRが使われた場合、攻撃に気づくのは難しいでしょう)
PR Creator | Last Commit Pusher | PR Approver |
---|---|---|
Alice | github-actions | Mallory |
攻撃パターン7を防ぐ方法
この攻撃はPR creator、last commit pusher、PR approverがすべて違うユーザーであり、これまで紹介したオプションを使用しても防ぐことができません。
GitHubが提供する方法でこの攻撃を防ぐにはPRのマージに必要な承認数(Required number of approvals before merging)を2以上に変更することです。しかしこの数を増やすことは開発者の生産性の低下につながり、あまり良い解決策とは言えません。
コードオーナーによるレビューを必須とするオプション(Require review from Code Owners)を使うことによりこの攻撃が行える可能性を減らすことは可能ですが、Malloryがコードオーナーであった場合は依然として攻撃が可能です。このオプションは攻撃の成功確率を下げることができるかもしれませんが、完璧な対策とはなり得ません。
現状、GitHubが提供する機能だけではこの攻撃を防ぐことはできないため、この攻撃パターンに対処したい場合はなんらかの仕組みを独自に開発する必要があります。例として以下のような方法が考えられます。
- 攻撃パターン7に合致するようなPRがマージされた場合にアラートを上げるような仕組みを作る
- PRのマージに必要な承認数を2にして攻撃パターン7に該当しない場合はbotがPRを承認し、botと人間1人による承認でPRがマージできるようにする
なお、この件については今年5月ごろにGitHubに報告済みであり、意図した挙動であるという回答をもらっています。またこの件をブログに取り上げることについても承諾を得ています。
まとめ
本記事ではGitHubのbranch protectionについて、その回避方法と対策について考察しました。Branch protectionは重要なブランチを守る強力な機能である一方で、GitHub Actionsを利用すると場合によっては突破が可能であり、完全なものではないということもわかりました。本記事が各組織や個人がGitHubをよりセキュアに利用する一助になれば幸いです。