理系学生日記

おまえはいつまで学生気分なのか

Pull RequestにテストカバレッジをコメントするGitHub Actionsを「セキュアに」実装するには

GitHubでPull Requestを作成することはもはや当たり前のように行われています。 最近は多くの場合、対象リポジトリをcloneした後でfeature branchを作成し、当該のfeature branchを用いてPull Requestを作成するという流れが一般的になったように思われますが、リポジトリをforkし、fork元へPull Requestを作成するという流れもまだまだ多くの場面で利用されています。

また、Pull Requestに反応する形でCIジョブが実行され、その結果をPull Requestにコメントするという運用も多くみられるようになりました。僕も、テストカバレッジをPull Requestにコメントしたいんだわ。

TL;DR

パブリックリポジトリでカバレッジレポートをPull Requestにコメントする場合、次のようなワークフローを組むのが良いっぽい。

  • pull_requestイベントでテストを実行して、カバレッジレポートをArtifactとしてアップロードする
  • その後にworkflow_runイベントを起動させ、カバレッジレポートをダウンロードし、その内容をPull Requestにコメントする

GitHub Actionsでのカバレッジコメント

問題

この両者を組み合わせるとき、つまりfork元へPull Requestを作成し、それをトリガとしてコメントを投稿するCIジョブが実行される場合に問題が発生することがあります。Pull Requestへのコメントができないんだわ。

Couldn't write a comment to the pull request. Please make sure your job has the permission 'pull-request: write'.

Original Error was: [HttpError] - Resource not accessible by integration

これは、GitHub Actionsが次のようにpull_requestイベントをトリガとしてCIジョブを実行している場合に発生します。

on:
  pull_request:
    types: [opened, synchronize]

原因

pull_requestイベントをトリガとしていることを前提として、fork先のリポジトリからfork元リポジトリへPull Requestを作成した場合、GITHUB_TOKENの権限は読み取り専用になります。

読み取り専用なのだから当然、Pull Requestにコメントを投稿することはできません。

これはなんでそうなっているかというとセキュリティ上の設計によるものです。悪意のある第三者が、悪意のあるコードをCIジョブで実行させたときの懸念をできるだけ抑えたいですもんね。

Due to the dangers inherent to automatic processing of PRs, GitHub’s standard pull_request workflow trigger by default prevents write permissions and secrets access to the target repository.

Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests | GitHub Security Lab

強制的に書き込み権限を与えれば解決するんだけど、それはセキュリティ上のリスクを伴います。 GitHub Actionsのワークフローファイルに悪意のあるコードを入れたら、それがCIジョブで実行されるわけなので、書き込み権限を如何様にも使えます。悪意のあるコメントでリポジトリを埋めることも、リポジトリ自体に悪さすることも可能ですね。

それならpull_request_targetイベントを使えば…

いくつか回避策はあります。そのうちで代表的なのはpull_requestではなくpull_request_targetイベントをトリガとしてCIジョブを実行することでしょう。この場合、利用されるGITHUB_TOKENには書き込み権限も付与されます。

pull_request_targetはfork元リポジトリのコンテキストで動作するので、Pull Requestを送ってきた人の任意のコードが実行されないというメリットもある(というか、こちらの方がpull_request_targetイベントが導入された背景であり、その結果として書き込み権限を付与できるようになった順という理解)。

pull_request_target イベントによってワークフローがトリガーされると、パブリック フォークからトリガーされた場合でも、GITHUB_TOKEN にはリポジトリの読み取り/書き込みアクセス許可が付与されます。

自動トークン認証 - GitHub Docs

であれば、pull_request_targetイベントを使えばいいよね、という話になりますが、そうは問屋が下さない。先の

pull_request_targetはfork元リポジトリのコンテキストで動作する

という記述は、CIでチェックアウトされ実行されるコードが、(修正を含まない)fork元リポジトリのコードであるということを意味しています。わかりづらいと思うので、こちらをみていただけると良いと思う。

要するに、Pull Requestに含まれる修正を含むコードはチェックアウトされないので、それに対してテストを実行しても意味がない。

workflow_runの登場

僕の目的に対してpull_request_targetイベントは使えません。

pull_requestイベントをトリガとするCIジョブでテストを実行し、カバレッジレポートを取得する。ここまでは良いのですが、コメントができない。書き込み権限を付与してコメントすることは可能ですが、そうすると悪意のあるユーザが悪意のあるコード込みでPull Requestを送ってきた時に、リポジトリにヤバい内容が混入したり、ヤバい内容がコメントされたりするリスクが出てくる。

そこで、pull_requestイベントでテストを実行し、カバレッジレポートをArtifactとしてアップロードする。可能な限り、ここでコードの安全性も確認する。その後にworkflow_runイベントを起動(連鎖)させ、カバレッジレポートをダウンロードし、その内容をPull Requestにコメントするという方法を取れば良い。

GitHub Actionsでのカバレッジコメント

workflow_runイベントにおいては、もはやPull Requestに含まれるコードとは隔離されています。このため、GITHUB_TOKENに書き込み権限を与えてもリスクは低い。

感想

今までずっと、特段の考慮をしないまま書き込み権限を与えてきたことが多いので、なるほどなと思いました。 余談だけど、リポジトリの設定には、fork先からのPull Requestに対してCIジョブを実行させるか否かの承認設定なんてものもあるので、別観点でのセーフガードになります。

fork先からのPRに対する権限設定

参考文献