
はじめに
こんにちは。GMO Flatt Security株式会社 セキュリティエンジニアの佐藤(@Nick_nick310)です。
本シリーズの前回記事では、GitHub Actionsに対する初期アクセスの手法と対策を解説しました。まだお読みでない方は、先に前回の記事をご覧いただくことをお勧めします。
GitHub Actionsのセキュリティを考える際、Script Injectionや信頼できないPull Requestのcheckoutといった初期アクセスに関わる問題が注目されることは少なくありません。しかし、実際の侵害を考えるうえでは、攻撃の入口だけでなく、その後にどのような権限昇格が可能かを見ることも重要です。初期アクセス時点では限定的に見える権限であっても、ワークフローの設定や認証情報の性質によっては、より強い権限を持つワークフローやsecretsへ到達できる場合があります。
本記事では、GitHub Actionsで用いられる各種認証情報や権限の特徴を整理したうえで、権限昇格につながる代表的なパターンを紹介します。あわせて、それらを防ぐためにどのような設定・運用が有効かを解説します。
各種権限について
GitHub Actionsでは複数の認証情報が使用されます。まずは各種権限の全体像を示します。詳細はそれぞれの章をご確認ください。
| 認証情報 | 影響範囲 | 権限の粒度 | 有効期限 | pushによるワークフロー起動 |
|---|---|---|---|---|
| GITHUB_TOKEN | 単一リポジトリ | ワークフローのpermissionsで制御 | ジョブ終了時または最大6時間 | 不可 (workflow_dispatch 等は可) |
| Classic PAT | ユーザーがアクセス可能な全リポジトリ | スコープ単位(粗い) | 手動設定(無期限も可) | 可 |
| Fine-grained PAT | 指定したリポジトリのみ | リポジトリ権限・Organization権限を個別指定 | 最大1年 | 可 |
| 外部コラボレーターのPAT | アクセスを許可されたリポジトリのみ | Classic PATのスコープに依存 | Classic PATに準ずる | 可 |
| GitHub App秘密鍵 | アプリがインストールされた全アカウント | アプリに付与された権限 | 無期限 | 可 |
| GitHub Appインストールアクセストークン | インストール先のリポジトリ | アプリに付与された権限 | 1時間 | 可 |
GITHUB_TOKEN権限の特徴
GITHUB_TOKENはrunner上で使用される認証情報です。GITHUB_TOKENはワークフローが実行されているリポジトリに対してのみ有効であり、Organization内の他のリポジトリには影響しません。また、ジョブの終了時またはGitHub hosted runnerでは最大6時間で失効するため、窃取後に利用できる時間にも制限があります。
ワークフローが侵害された場合、攻撃者がまず手にするのはGITHUB_TOKENです。このGITHUB_TOKENで何ができるかは、ワークフローに設定されたpermissionsに依存します。
GITHUB_TOKENの権限が広い場合、単一リポジトリ内で生じうる被害が大きくなります。permissionsが未設定のワークフローでは、デフォルトの権限が適用される仕組みです。
この時のデフォルト権限はリポジトリの設定によりますが、forkからのpull_requestトリガーを除き、contents: write、packages: write、actions: writeなど広範な権限が付与される可能性があります。
2023年2月以降は、デフォルトでは読み取り権限のみが付与されるようになりました1。
ただし、このポリシー変更が適用されるのはそれ以降に新規作成されたリポジトリのみで、既存リポジトリには自動的に反映されません。

また、GITHUB_TOKENを使った操作は、原則として新しいワークフローの実行をトリガーしないという仕様があります2。
GITHUB_TOKENを使ってgit pushを実行しても、pushトリガーやpull_requestトリガーのワークフローは起動しません。
この制約により、攻撃者がGITHUB_TOKENを使ってリポジトリへコードをpushしたとしても、それを契機に別のワークフロー(例えばデプロイワークフロー)を連鎖的に起動させることはできません。
例外として、 workflow_dispatchとrepository_dispatchはトリガー可能です。
nxの侵害事例では、窃取したGITHUB_TOKENで新規ブランチを作成した後、workflow_dispatchをAPI経由でトリガーする手法が使われました3。
この例外の存在は攻撃者にとって大きな意味を持ちます。contents: write権限があれば新規ブランチの作成とワークフローファイルの配置が可能であり、actions: write相当の権限(デフォルト権限が広い場合に含まれる)があればworkflow_dispatchのトリガーも可能です。つまり、攻撃者はGITHUB_TOKEN単体で「secretsを外部に送信する悪意あるワークフローを新規ブランチに作成し、workflow_dispatchで実行する」という攻撃チェーンを完結させることができます。ブランチ保護ルールが設定されていても、保護対象外のブランチを新規作成すれば迂回可能です。

まとめると、GITHUB_TOKENが漏洩した場合の影響範囲は単一リポジトリに限定されますが、広くて強い権限が設定されている場合は前述の攻撃チェーンが成立するため、漏洩時のリスクは高いと言えます。
Classic PATの特徴
Classic PAT(Personal Access Token)はユーザーに紐づくトークンです。所有者がアクセス可能なすべてのリポジトリに対して有効で、repoスコープのように粗い権限単位でしか制御できず、特定のリポジトリだけに制限する仕組みがありません4。
また、GitHubのOrganizationには Base permissionsという設定があり、Classic PATのデフォルト権限を設定することが可能です。2026年6月19日現在、この設定はデフォルトで Readになっています。
Base permissionsが Writeの場合、Organization内のリポジトリに対して Write権限を持つことになります。前述の通りClassic PATは粗い単位でしか設定できないため、 repoスコープを有効にすると ContentsやActions権限などまとめて Writeに設定される点に注意が必要です。
このようなOrganizationでClassic PATが漏洩すると、Organization内の全リポジトリに対してスコープで設定された機能のWrite権限を奪取されることになります。
なお、リポジトリごとに権限を絞る手段としてteam機能がありますが、team機能で管理できるのはBase permissionsより上の権限を付与する場合に限られ、Base permissionsを下回る権限に制限することはできません5。
Fine-grained PATの特徴
Fine-grained PATはClassic PATと比較して影響範囲を大幅に制限できます。GitHub公式ドキュメントによると、Fine-grained PATには以下の機能があります。
- アクセス可能なリソースオーナー(ユーザーまたはOrganization)を指定できる
- アクセス可能なリポジトリを個別に指定できる(「All repositories」または「Only select repositories」)
- 権限をリポジトリ権限・Organization権限それぞれで細かく指定できる
Fine-grained PATが漏洩した場合、攻撃者の行動範囲はトークン作成時に指定されたリポジトリと権限に制限されます。
行動の例を挙げると、workflow_dispatchトリガーの使用は、Fine-grained PATの場合 Actionsのwrite権限が必要です。Contentsのwrite権限がなければワークフローファイルの書き換えもできません。
ただし、Fine-grained PATにも制約があります。
- 外部コラボレーターはFine-grained PATを使用してOrganizationリポジトリにアクセスできない(Classic PATのみ対応)
- 一部のAPIへのアクセスはFine-grained PATでは未対応
これらの制約が、一部の開発者がClassic PATから離れられない要因の一つとなっています。
外部コラボレーターの特徴
外部コラボレーターは、Organizationのメンバーではないものの、特定のリポジトリへのアクセスを許可されたユーザーです。コンサルタントや一時的な外注先に対して権限を付与する際に使用されます。
また、Base permissions は外部コラボレーターには適用されません。外部コラボレーターはリポジトリ単位で明示的にアクセスレベルが設定され、許可されたリポジトリにのみアクセス可能です。

外部コラボレーターのPATが漏洩した場合、影響範囲はアクセスを許可されたリポジトリに限定されます。しかし、前述の通り外部コラボレーターはFine-grained PATを使用することができないため、PATを使いたい場合はスコープ粒度が粗いClassic PATを使用するしかありません。Classic PATのrepoスコープが有効であれば、ContentsやActionsなどへのwrite権限が付与され、リポジトリに対して各種操作が可能になります。
GitHub Appの秘密鍵の特徴
GitHub Appには2種類の認証情報があります。秘密鍵(Private Key)とインストールアクセストークンです。 秘密鍵はアプリの登録時に生成され、インストールアクセストークンの発行に使用されます6。
秘密鍵は有効期限がなく、アプリがインストールされた全アカウントへのアクセスを許可します。
インストールアクセストークンは秘密鍵から生成される短命のトークンで、1時間で失効します7。インストール先のリポジトリに対してのみ有効であり、漏洩時の影響範囲はClassic PATや秘密鍵よりも限定的です。
複数リポジトリにまたがる操作など、GITHUB_TOKENでは実現できない用途のために、GitHub Appの秘密鍵をワークフローのsecretsに保存し、インストールアクセストークンを発行する構成は広く使われています。GitHub公式のactions/create-github-app-tokenも、秘密鍵からインストールアクセストークンを発行するこの方式を前提としています8。
秘密鍵が漏洩すると、攻撃者はそのアプリがインストールされているすべてのアカウントに対してインストールアクセストークンを発行できるようになります。アプリを複数のOrganizationに横断してインストールしているケースでは、GITHUB_TOKENの漏洩が単一リポジトリに限定されるのと比べ、影響範囲が大幅に広がります。さらに秘密鍵には有効期限がないため、漏洩に気づかなければ長期間にわたって悪用される可能性があります。

権限昇格の手法
この章では権限昇格(Privilege Escalation)の脆弱性を紹介します。
権限昇格のフェーズでは、初期アクセスで得た権限を超える操作を狙います。初期アクセスの時点では権限は制限されている場合が多々ありますが、脆弱性を悪用することで、より強い権限を持つワークフローやsecretsへアクセスできる可能性があります。
GITHUB_TOKENで広範なデフォルト権限を使用している
前述の通り、 ワークフローでpermissionsを明示的に設定していない場合、GITHUB_TOKENにはデフォルト権限が付与されます。
pull_requestトリガーの場合はforkからの実行時に権限が制限されますが、その他のトリガーにはこの制限がありません。前回の記事で紹介したScript Injectionや信頼できないPull Requestのcheckoutと組み合わさることで、広範なデフォルト権限がそのまま攻撃者に渡ることになります。
キャッシュポイズニング
GitHub Actionsにはワークフローの実行を高速化するためのキャッシュ機能があります。actions/cacheを使用すると、node_modulesなどの依存関係をキャッシュし、以降の実行で再利用できます。
キャッシュの参照にはスコープの制限があり、GITHUB_REF(ブランチ)に基づいて参照可能な範囲が決まります。ただし、デフォルトブランチで作成されたキャッシュは、すべてのブランチから参照可能です。また、キャッシュの作成自体にはワークフローの権限による制限がなく、書き込み権限が絞られたワークフローからでもキャッシュの書き込みは可能です。
キャッシュキーについて、既に存在する場合は同じキーで上書きすることができません。
しかし、リポジトリのキャッシュ容量が10GBを超えると、古いキャッシュから順に削除される仕様があります9。攻撃者はこの仕様を悪用し、大量のジャンクデータでキャッシュ領域を埋めることで既存のキャッシュを削除させます。その後、悪意あるキャッシュを同じキーで作成すれば、後続のワークフローがそのキャッシュを使用することになります。
clineの侵害では、AIエージェント経由でrunner上のコマンド実行を獲得した後、このキャッシュポイズニングを用いて権限の高いリリースワークフローを侵害しました。
権限が絞られたissueトリガーなど権限の低いワークフローから、secrets を持つリリースワークフローへの橋渡しとして利用される点が、この手法の特徴です。

再利用可能なワークフローを呼び出す際にsecrets: inheritを使用している
再利用可能なワークフローを呼び出す際、secrets: inheritを設定すると、呼び出し元のsecretsがすべて暗黙的に渡されます。
以下はこの設定をしているワークフローの例です。
jobs:
call-workflow:
uses: ./.github/workflows/reusable.yml
secrets: inherit # 呼び出し元の全secretsが渡される
この設定は個別のsecretsを列挙する煩わしさから解放されるため、利便性の観点で多用されがちです。しかし、再利用可能なワークフローが侵害された場合(例えば外部リポジトリのReusable Workflowを使用しており、そのリポジトリが侵害された場合)、攻撃者は呼び出し元が持つsecretsをすべて取得できます。
権限昇格を防ぐ
ここまで紹介した権限昇格の手法に対して、実践できる対策を解説していきます。まずは対策の全体像を示します。詳細はそれぞれの章をご確認ください。
- ワークフローの
permissionsは最小権限で明示的に設定する。- ワークフロー冒頭で
permissions: {}を宣言し、ジョブごとに必要な権限だけを付与する。Organizationレベルでデフォルト権限を読み取りのみに制限することも可能
- ワークフロー冒頭で
- OrganizationでClassic PATの使用を制限する。
- Classic PATはスコープが粗く、漏洩時に全リポジトリへの影響が及ぶ。Fine-grained PATのみを許可するポリシーに変更する
- Base permissionsを
No permissionまたはReadに設定する。Writeに設定されている場合、メンバーのPATが1つ漏洩するだけでOrganization内の全リポジトリに書き込みが可能になる
- GitHub Appの秘密鍵はsecretsではなくクラウドの鍵管理システムに保存する。
- 秘密鍵は有効期限がなく、インストール先の全アカウントにアクセスできるため、漏洩時の影響範囲が最も広い
- 外部コラボレーターの権限は原則
Readにとどめ、定期的に棚卸しする。- Fine-grained PATが使用できないため、Classic PATによるアクセスの影響範囲に注意が必要
- secretsやクラウド認証を扱うワークフロー、リリースワークフローではキャッシュを使用しない。
- キャッシュの参照スコープを制限する設定は存在せず、キャッシュポイズニングによる権限昇格を防ぐ手段がない
- 再利用可能なワークフローでは
secrets: inheritを使用せず、必要なsecretsだけを明示的に渡す。secrets: inheritは呼び出し元の全secretsを暗黙的に渡すため、侵害時の影響範囲が不必要に広がる
権限に関連する設定を堅牢にする
permissionsを明示的に最小権限で設定する
前述の通りワークフローにpermissionsを宣言していない場合、GITHUB_TOKENにはリポジトリのデフォルト設定に応じた権限が付与されます。
対策として、ワークフローの冒頭でpermissions: {}を宣言し、ジョブごとに必要な権限だけを明示的に付与します。
以下はジョブごとに必要な権限を付与したワークフローの例です。
# ワークフロー冒頭で全権限を無効化
permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read # コードの読み取りのみ
steps:
- uses: actions/checkout@v4
- run: npm run lint
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC認証に必要
steps:
- uses: actions/checkout@v4
# ...
デフォルトの権限設定はOrganization全体で強制することも可能です。OrganizationのSettings > Actions > Generalから、GITHUB_TOKENのデフォルト権限を「Read repository contents and packages permissions」に変更し、ワークフローにおける過剰な権限の設定を防止するオプションを有効にできます。
OrganizationでClassic PATの使用を制限する
Classic PATはrepoスコープを持つだけでユーザーがアクセス可能な全リポジトリに対して操作でき、権限昇格や横展開の最大の要因となります。Classic PATを使用する必要がない場合は、Organizationの設定でClassic PATを制限することが可能です。
具体的には、OrganizationのSettings > Personal access tokens > Settingsから、以下のポリシーを設定可能です。
- Restrict access via personal access tokens (classic): Classic PATによるOrganizationリソースへのアクセスを禁止する。この設定を有効にすると、Classic PATでOrganization配下のリポジトリにアクセスしようとした際に403エラーが返される
- Allow access via personal access tokens (classic): Classic PATによるアクセスを許可する(デフォルト)
Classic PATの使用を制限し、Fine-grained PATのみを許可することで、漏洩時の影響範囲をトークン作成時に指定したリポジトリと権限に限定できます。

Fine-grained PATに対しては、さらに以下のポリシーも設定できます。
- Require administrator approval: Fine-grained PATがOrganizationにアクセスする際に、Organization ownerの承認を必須にする(デフォルト)
- Maximum lifetime policy: Fine-grained PATの最大有効期限を設定する(デフォルトは366日以内)
ただし、外部コラボレーターはFine-grained PATでOrganizationリポジトリにアクセスできないという制約があります。Classic PATを制限する場合、外部コラボレーターのアクセス手段についてはGitHub AppやDeploy Keyなどの代替手段を検討する必要があります。
Base permissionsを最小にする
OrganizationのBase permissionsがWriteに設定されている場合、メンバーのPATが1つ漏洩するだけでOrganization内の全リポジトリに書き込みが可能になります。
デフォルトではメンバーはOrganization内のパブリックリポジトリに対してRead権限を持ちます。Base permissionsはNo permissionまたはReadに設定し、リポジトリへの書き込み権限はチーム単位で必要なリポジトリにのみ付与する運用が安全です。

Base permissionsは外部コラボレーターには適用されません。外部コラボレーターはリポジトリ単位で明示的にアクセスレベルが設定されるため、Base permissionsの変更による影響はありません。
内部リポジトリ(GitHub Enterprise Cloudの機能)を使用している場合、Base permissionsをNo permissionに設定しても内部リポジトリには最低でもReadが付与される点に注意が必要です。
外部コラボレーターの権限を制限する
外部コラボレーターにはBase permissionsが適用されないため、リポジトリ単位で明示的に設定された権限がそのまま影響範囲になります。
外部コラボレーターに対しては以下を行うことでリスクの軽減が可能です。
- リポジトリへの権限は原則
Readにとどめ、Writeが必要な場合はリポジトリとその理由を限定する - 契約終了時にはアクセスを速やかに削除する。外部コラボレーターの棚卸しは定期的に実施する
GitHub Appの秘密鍵を安全に管理する
GitHub Appの秘密鍵はアプリがインストールされた全アカウントへのアクセスを許可し、有効期限もないため、漏洩時の影響範囲が最も広い認証情報の一つです。
そのため、secretsに保存するのではなく、クラウド上の鍵管理システムに保存することが推奨されています10。GitHub公式はAzure Key Vaultを例として挙げています。
重要度が高いワークフローではキャッシュを使用しない
GitHub Actionsのキャッシュには、権限で制御できない構造的な問題があります。
キャッシュの作成はどのワークフローからも可能であり、デフォルトブランチで作成されたキャッシュは全ブランチから参照できます。さらに、キャッシュ領域が10GBを超えると古いキャッシュから削除される仕様があるため、正規のキャッシュを意図的に追い出して悪意あるキャッシュに差し替える攻撃(キャッシュポイズニング)が成立します。
キャッシュの参照スコープを制限する設定は存在しないため、secretsやクラウド認証を扱うワークフロー、リリースを実行するワークフローではキャッシュを使用しないことが対策となります。
secrets: inheritを使用しない
前述の通り、再利用可能なワークフローを呼び出す際に secrets: inherit を指定すると、呼び出し元が持つすべてのsecretsが暗黙的に渡されます。
必要なsecretsだけを明示的に渡すことで、影響範囲を限定できます。
# 安全な例: 必要なsecretsだけを明示的に渡す
jobs:
call-workflow:
uses: ./.github/workflows/reusable.yml
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
コラム:プライベートリポジトリのリスクについて
ここまでで権限昇格の手法と対策を紹介してきました。
前回の記事と合わせてみると、「ほとんどの内容がパブリックリポジトリの話ではないか?」と考えられるかもしれません。実際、その通りです。
では、ここでプライベートリポジトリにおけるリスクを考えてみましょう。
プライベートリポジトリは、前回の記事にて紹介した初期アクセス手段のほとんどが使用できないため、侵害されるとすれば、なんらかの要因で漏洩した認証情報からのアクセスが起点となります。 (いわゆる内部起点のペネトレーションテストと考え方は近いです。)
では、漏洩した認証情報はどれほどのリスクがあるのでしょうか。
まず、各種認証情報において、read権限があるものはデータが読み取られます。
パブリックリポジトリでは影響がなかったcontents: readのような設定も、ソースコードの漏洩に繋がってしまいます。そのため、プライベートリポジトリでは、機密情報が含まれるリソースに対しての読み取り権限も慎重に付与する必要があります。
次にwrite権限について考えます。
GITHUB_TOKENにおいて、write権限の一部であるpull-requests: writeやissues: writeは、一見するとリスクが低い権限に思えます。
既存のPull RequestやIssueの改竄・削除は可能ですが、リポジトリの内容を直接書き換えることはできません。
GITHUB_TOKENに限ればこの評価はおおむね妥当です。GITHUB_TOKENでPull RequestやIssueを作成してもpull_requestトリガーやissuesトリガーは起動しません。ワークフローの連鎖が起きないため、これらの権限を使用しての権限昇格は困難です。
しかし、Classic PATとFine-grained PATには、GITHUB_TOKENのようなトリガー抑制の仕組みがありません。PATでPull Requestを作成すればpull_requestトリガーのワークフローが実行され、Issueを作成すればissuesトリガーのワークフローが実行されます。
ここで問題になるのは、トリガーされたワークフロー側の権限です。pull-requests: writeのみのFine-grained PATが漏洩した場合、攻撃者はリポジトリの内容を直接書き換えることはできません。しかしPull Requestを作成すれば、pull_requestトリガーで起動するワークフローがcontents: writeやid-token: writeなど広い権限で動作する可能性があります。そのワークフローにScript Injectionが存在すれば、攻撃者はワークフロー経由で権限昇格を達成できてしまいます。
以上から、「プライベートリポジトリだから外部からの攻撃はない」と考えるのは危険だと言えます。
そのため、プライベートリポジトリであっても、各種対策は実施すべきです。

おわりに
GitHub Actionsにおける権限昇格は、単に強い権限を持つ認証情報の漏洩だけで発生するものではありません。GITHUB_TOKEN、PAT、GitHub App、再利用可能なワークフロー、キャッシュといった各要素の性質が組み合わさることで、限定的に見える権限からでも、より強い権限へ到達できる場合があります。
そのため、個々のワークフローに脆弱性がないかを見るだけでなく、「この認証情報が漏洩したとき、どこまで到達できるか」「このトリガーで起動するワークフローは、過剰な権限を持っていないか」といった観点で設計全体を見直すことが重要です。
GitHub Actionsは非常に強力な仕組みです。その一方で、設定や運用のわずかな差が、侵害時の影響範囲を大きく左右します。
本記事が、GitHub Actionsの権限設計や認証情報管理を見直す際の参考になれば幸いです。
ここまでお読みいただきありがとうございました。
- https://github.blog/changelog/2023-02-02-github-actions-updating-the-default-github_token-permissions-to-read-only/↩
- https://docs.github.com/en/actions/concepts/security/github_token#when-github_token-triggers-workflow-runs↩
- https://nx.dev/blog/s1ngularity-postmortem↩
- https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens↩
- https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/setting-base-permissions-for-an-organization#about-base-permissions-for-an-organization↩
- https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps↩
- https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app↩
- https://github.com/actions/create-github-app-token↩
- https://github.com/actions/cache#cache-limits↩
- https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps#storing-private-keys↩