public リポジトリの PR と CI の権限まわり
public リポジトリにブランチ保護をかけていないことが気になって調べた内容の書き留め。
第三者は PR をマージできない
public リポジトリでも、fork から PR を出せるのと、それをマージできるのは別の権限になっている。マージには write 権限が要るので、権限を渡していない相手は PR を出すところまでしかできない。
ブランチ保護(ruleset)が防ぐのは第三者ではなく、自分の事故のほう。main への直 push、force push、CI が赤いままのマージ、ブランチの削除がそれにあたる。AI エージェントにリポジトリ操作をさせる場合はここが効いてくるが、「public だから危ない」という話とは別物。
権限が動くのは CI の実行のほう
fork PR で問題になるのは、マージではなくワークフローの実行。トリガーによって、渡されるものが違う。
表1: トリガー別に渡される権限
| トリガー | 実行されるコード | secrets | GITHUB_TOKEN |
|---|---|---|---|
pull_request | PR(fork)側 | 渡されない | read-only |
pull_request_target | base(main)側 | 渡される | write |
workflow_run | base 側 | 渡される | write |
issue_comment | base 側 | 渡される | write |
pull_request は安全側に倒してある。fork からの PR で任意のコードが走っても、そこに secrets は無いし、トークンも読み取り専用。
pull_request_target で PR 側を checkout すると成立する
危険なのは、base 側の権限で走る pull_request_target の中で、PR 側のコードを取ってきて実行する形。
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: $ # PR 側のコードを取る
- run: npm ci && npm run build # secrets のある環境で実行される
env:
API_TOKEN: $npm ci は package.json の postinstall を走らせるし、ビルドスクリプトも PR 側が書き換えられる。つまり誰でも、こちらの secrets が入った環境で任意のコードを実行できる。デプロイ用のクラウド API トークンや、他リポジトリへ書ける PAT が置いてあれば、そのまま持っていかれる。
$ のような、投稿者が中身を決められる値を run: に直接展開するのも同じ系統。シェルに文字列として展開されるので、タイトルにコマンドを仕込める。
見るところ
.github/workflowsの中でpull_request_target/workflow_run/issue_commentを使っているものを洗い出す- 該当したら、その中で PR 側の ref を checkout していないか、secrets を参照する job に fork PR から到達できないかを見る
- secrets を持っている public リポジトリを一覧する。トークンの種類によって、漏れたときの範囲が変わる(デプロイ先だけで済むものと、他リポジトリまで書けるものがある)
- 外部 action をタグ参照(
@v1、@main)にしているものは、secrets を扱う job だけでも commit SHA 固定にする
secrets が 0 件のリポジトリは、この経路では取られるものが無い。まずは secrets を持っているものから見ればいい。