
- はじめに:なぜ今、GitHub Actionsのセキュリティなのか
- GitHub Actionsが侵害されると何が起きるか
- GitHub Actionsに対する攻撃の事例分析
- 初期アクセスの手法紹介
- 初期アクセスを防ぐ
- コラム:依存関係の汚染による初期アクセスについて
- 終わりに
はじめに:なぜ今、GitHub Actionsのセキュリティなのか
こんにちは。GMO Flatt Security株式会社 セキュリティエンジニアの佐藤(@Nick_nick310 )です。
近年、GitHub Actionsを標的としたサイバー攻撃の脅威が急速に顕在化しています。2025年から現在にかけて、tj-actions1、nx2、trivy3、そしてcline4といった著名なプロジェクトにおいてGitHub Actionsの侵害事例が相次いで報告されました。また、2026年4月には、pull_request_target トリガーの設定不備を抱えたリポジトリを特定するための、大規模な探索活動も観測されています。
GitHub Actionsは現代のソフトウェア開発において不可欠なCI/CD基盤の一つとなっており、多くのオープンソースプロジェクトや企業が日常的に利用しています。しかし、GitHub Actionsは設計思想として高い利便性と柔軟性を志向しているため、デフォルトの状態では「最小権限の原則」が十分に適用されていないケースが散見されるのが現状です。
セキュアな開発環境を維持するためには、プラットフォーム側の提供機能に依存するだけでなく、ユーザー側が能動的に設定を見直すことが不可欠です。
本シリーズでは全4回にわたり、GitHub Actionsのセキュリティについて体系的に解説します。第1回となる本記事では、近年の具体的な侵害事例の分析を通じて、GitHub Actionsが侵害される主要な経路やリスクを整理し、初期アクセスに対する防御策について解説します。
GitHub Actionsが侵害されると何が起きるか
GitHub Actionsは、コードのビルドやテストの自動実行、本番環境へのデプロイ、npmやDockerレジストリへのパッケージ公開など、開発を幅広く支えるCI/CD基盤です。
GitHub Actionsが侵害されると、攻撃者はワークフローのrunner上で任意のコマンドを実行できるようになります。GitHub-hosted runnerは一時的な環境ですが、ワークフロー実行中にはGITHUB_TOKEN、secretsとして登録されたデプロイトークンやAPIキー、OIDC認証で取得した一時的なクラウドクレデンシャルといった認証情報がメモリ上に存在しており、攻撃者はこれらをrunner上から窃取することができます。
パッケージの公開やコンテナイメージのpushといったリリース処理も、runner上のコマンドとして実行されています。攻撃者がリリースワークフローに到達すれば、正規のリリースフローをそのまま悪用できます。nxの侵害では窃取したNPM_TOKENで悪意あるパッケージがnpmに公開され、trivyの侵害では開発元の公式自動リリースシステムを経由して改竄されたバイナリが配布されました。
厄介なのは、runner上でのコマンド実行を許した時点で認証情報の窃取を完全に防ぐ手段がないことです。そのため、侵害自体を防ぐことと、侵害された場合の影響範囲を最小化することの両面で対策を講じる必要があります。

GitHub Actionsに対する攻撃の事例分析
GitHub Actionsを標的とした攻撃は、大きく分けて「脆弱なトリガー構成を悪用したインジェクション」や「タグ汚染によるソフトウェアサプライチェーン攻撃」の2つのパターンによるものが代表的です。近年ではAIエージェントをGitHub Actionsでも使用する例が見られるため、これらに加えて「AIエージェントへの過剰な権限付与」も重要な観点となります。それぞれのパターンを見ていきましょう。
脆弱なトリガー構成を悪用したインジェクション
GitHub Actionsワークフローでは、ブランチ名やコミットメッセージといったPull Request由来の値を外部コンテキスト(${{}})として参照できます。
この外部コンテキストではエスケープ処理が存在しないため、runフィールドで直接展開していると、外部コンテキスト経由でシェルコマンドが実行されるScript Injectionに繋がります。
Ultralytics(2024年12月)
画像認識ライブラリとして広く利用されているUltralyticsが、この手法によりGitHub Actionsの侵害を受けました5。
該当ワークフローではpull_request_targetトリガーを使用しており、permissionsが明示的に設定されていませんでした。
攻撃者はこのGITHUB_TOKENやその他の情報を窃取し、暗号通貨マイナーを含んだビルドへの改竄を行いました。
nx(2025年8月)
モノレポ管理ツールであるnxが、Ultralyticsと同様の手口で侵害されました6。
攻撃者は高い権限を持つGITHUB_TOKENを利用してnpm tokenを窃取し、悪意あるコードを含んだパッケージをnpmに公開しました。公開された悪意あるパッケージには、インストール先の環境からGitHub tokenやnpm tokenを窃取するコードが含まれており、2次被害、3次被害へと連鎖していきました。nxのインシデント報告では、脆弱なワークフローがマージされた当時「セキュリティリスクを完全に把握できていなかった」と発表されています。
タグ汚染によるサプライチェーン
GitHub Actionsでは、外部Actionを参照する際はv1のようなタグを指定するのが一般的ですが、タグに紐づくcommitは付け替えることが可能です。攻撃者が外部Actionのリポジトリへの書き込み権限を取得した場合、既存タグを悪意あるcommitに付け替え、そのアクションを利用するすべてのワークフローで悪意あるコードを実行させることができます。
tj-actions/changed-files(2025年3月)
tj-actions/changed-files7がこの手法によりGitHub Actionsの侵害を受けました8。
攻撃者はまずreviewdog Organizationのメンテナーが使用していたPAT(Personal Access Token)を窃取し、reviewdogが提供するActionsのタグを悪意あるcommitに付け替えました。
reviewdogに依存していたtj-actionsのワークフローがこの悪意あるActionを実行した結果、tj-actions側のPATも窃取され、tj-actions/changed-filesのタグが改竄されるに至りました。

改竄されたActionにはRunner.Workerプロセスのメモリを読み取ってsecretsをダンプするPythonスクリプトが仕込まれており、ワークフローのsecretsがビルドログに出力される結果となっています。パブリックリポジトリではビルドログが公開されているため、ログを閲覧するだけでsecretsを取得可能な状態でした。発覚後、GitHubはtj-actions/changed-filesリポジトリを一時的に削除し、全バージョンのタグをクリーンなcommitに復元する対応を行っています。
trivy(2026年2月)
脆弱性スキャナとして広く利用されているtrivyが侵害されました。攻撃の起点は、hackerbot-clawと呼ばれるキャンペーンによるpull_request_targetワークフローの悪用です。Ultralyticsやnxの事例と同様の手法でワークフローを悪用し、PATを窃取しました。
窃取したPATを使って、リポジトリの非公開化とリネーム、v0.27.0からv0.69.1までの全GitHub Releasesの削除、VS Code拡張への不正なアーティファクトの公開が行われました。
3週間後の3月19日、2度目の侵害が発生したことをStepSecurity社が公開しました9。
2度目の攻撃では、悪意あるtrivyバイナリ(v0.69.4)が開発元の公式自動リリースシステム(aqua-bot)経由で公開されたほか、Imposter Commits(フォーク側のコミットを元リポジトリのコミットとして参照させる手法)を利用してaquasecurity/trivy-actionのタグ(0.35.0を除く0.0.1〜0.34.2)が悪意あるcommitに付け替えられました。改竄されたアクションにはRunner.Workerプロセスのメモリからsecretsを読み取り、RSA暗号化した上で外部ドメインに送信するクレデンシャルスティーラーが含まれていました。
AIエージェントへの過剰な権限付与
GitHub ActionsでAIエージェントを利用する場合、エージェントに許可するツールやユーザーの範囲がそのまま攻撃対象面になります。prompt injectionによりエージェントの挙動を操作されると、許可されたツールの権限の範囲で任意のコマンドを実行される恐れがあります。
2026年2月、VS Code拡張のAIコーディングエージェントであるclineが侵害されました10。攻撃の起点となったのは、issuesイベントで動作するIssue自動トリアージ用のワークフローです。このワークフローではclaude-code-actionが使用されており、allowed_non_write_users: "*"(誰でも実行可能)かつ--allowedToolsにBashを含む広範なツールが許可されていました。
攻撃者はIssueのタイトルにprompt injectionを仕込み、AIエージェントに悪意あるnpm installを実行させることでrunner上での任意コード実行を獲得しています。
さらに、キャッシュポイズニングによってNightlyリリースワークフローへ攻撃を拡大しました。
その結果、VSCE_PAT、OVSX_PAT、NPM_RELEASE_TOKENといった機密情報の窃取が行われ、2月17日には不正なcline@2.3.0がnpmに公開される事態に至りました。
初期アクセスの手法紹介
GitHub Actionsへの攻撃は脆弱性の組み合わせで行われているため、単体でみた時にリスク評価がしにくいものとなっています。
今回は「どのフェーズで使用されるか」をわかりやすくするために、MITRE ATT&CKのTacticsに則って紹介します。
本記事では初期アクセス(Initial Access)に使用される攻撃手法を紹介します。
初期アクセスのフェーズとは、名前の通り攻撃者が足掛かりとなる点を確保するフェーズです。なお、フィッシングも初期アクセスの1つですが、本記事の文脈とは逸れるため今回は対象外としています。
信頼できないPull Requestのcheckoutを悪用した任意コード実行
pull_request_targetやpull_requestトリガーでは、github.event.pull_request.head.shaを使用することでPull Requestのcommitを取得できます。この機能を使ってactions/checkoutを実行すると、Pull Request送信者が用意したコードがrunner上にcheckoutされた状態で後続の処理が進みます。
パブリックリポジトリの場合、誰でもfork経由でPull Requestを送信できます。これは攻撃者も例外ではないため、悪意あるコードを含んだPull Requestを自由に送信可能です。これらの挙動を組み合わせると、例えば攻撃者がpackage.jsonのpreinstallスクリプト等に攻撃コードを含んだ状態でPull Requestを送信することで、checkoutされたブランチでnpm installが実行され、結果的にGitHub Actions上で攻撃者の用意した任意のコードが実行されます。
脆弱な構成の例:
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install
- run: npm test
forkからのPull Requestに対して、pull_request_targetトリガーは強いデフォルト権限を持ち、pull_requestトリガーは制限された権限を持ちます。pull_requestトリガーには、read権限のみに制限される・secretsにアクセスできないという特徴があります。
pull_request_targetトリガーと比べ、pull_requestトリガーは比較的安全に使用できますが、ワークフロー上でのコマンド実行を起点とする権限昇格手法があるため、pull_requestトリガーを使用している場合でも無条件に安全と言えない点に注意が必要です。

外部コンテキストを直接使用することによるScript Injection
ワークフローのrunフィールドやscriptフィールドは、外部コンテキスト(${{ }})を参照する際に危険な文字列をエスケープしません。
例えば、以下のようなワークフローがあったとします。
脆弱なコード:
jobs:
test_job:
runs-on: ubuntu-latest
steps:
- name: Print issue title
run: echo "Processing issue: ${{ github.event.issue.title }}"
Issueのタイトルに "; echo "hello" # という文字列が設定された場合、runフィールドでは以下のコマンドが実行されます。
echo "Processing issue: "; echo "hello" #"
これにより攻撃者のスクリプトがrunner上で実行されます。Ultralyticsやnxの事例では、このScript Injectionにより初期アクセスを奪取されました。
信頼できないartifactを未検証で使用することによる任意コード実行
GitHub Actionsではファイルをartifactとしてアップロードし、別のワークフローでダウンロードして使用できます。
artifactはzip形式でアップロードされ、ダウンロード時に展開されます。展開先のディレクトリはpathが設定されていない場合は、GITHUB_WORKSPACEまたはカレントディレクトリが使用されます。
しかし、artifactのアップロード元は必ずしも信頼できるワークフローとは限りません。
pull_requestトリガーはPull Requestのコンテキストで実行されるため、すでに存在する pull_requestトリガーのワークフローを修正するPull Requestを送信すると、修正した内容でワークフローが実行されます。
pull_requestトリガーでは、前述の通りforkからのリクエストに書き込み権限は与えられないようになっており、かつsecretsへアクセスすることはできません。しかし、その一方でartifactのアップロードは行えるため、artifactを取り扱う際は信頼できないものとして検証を行う必要があります。
また、展開時にpathが指定されていなければ、actions/checkoutで取得したファイルがartifact内のファイルで上書きされる可能性があります。
以下が脆弱なワークフローの例です。後続の処理でシェルスクリプトを実行している場合、artifactを通じてそのシェルスクリプトが上書きされ、攻撃者が用意したスクリプトが実行されます。
脆弱な構成の例:
name: "Vulnerable Workflow"
on:
workflow_run:
workflows: ["Test Run"]
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout trusted code
uses: actions/checkout@v4
- name: before
run: cat ./script/deploy.sh
- name: Download Artifact from PR workflow
uses: actions/download-artifact@v4
with:
name: test-data # (A)アップロードされた「test-data」を取得する
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Set permission
run: chmod +x ./script/deploy.sh
- name: Deploy with powerful Secret
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "Processing data from artifact..."
RESULT_DATA=$(cat test-results/result.txt) # (C)artifactのデータを使用する
ls -la
ls -la ./script
ls -la test-results/
./script/deploy.sh "$RESULT_DATA" # (B) スクリプトを実行する
このワークフローでは、 (A)test-dataというartifactを取得後、 (B)./script/deploy.shを実行しています。フローの中で使用しているのは (C)test-results/result.txtですが、artifactに ./script/deploy.shを含めることで、任意のコマンドを実行することが可能です。
実際に、このワークフローのトリガーとなるTest Runワークフローに以下のような変更を加えてPull Requestを送信すると、変更後のワークフローが動作し、任意のコマンドが実行されます。
name: "Test Run"
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout PR code
uses: actions/checkout@v4
- name: Run tests
run: |
mkdir -p test-results
echo "PASS" > test-results/result.txt
- name: Inject malicious deploy script # 変更点:script/deploy.shの内容を変更する
run: |
mkdir -p script
cat << 'EOF' > script/deploy.sh
#!/bin/bash
echo pwned!
EOF
chmod +x script/deploy.sh
- name: Upload poisoned artifact # 変更点:scriptディレクトリもartifactに含める
uses: actions/upload-artifact@v4
with:
name: test-data
path: |
test-results/
script/
以下は実行時のActionsのログです。./script/deploy.shの内容が、 name:beforeのstepと、name:Deploy with powerful Secretのstepで異なっていることが確認できます。

AIエージェントへの過剰な権限の付与
2025年頃から、GitHub Actions上でAIエージェントを活用するケースが増えてきました。Issueの自動トリアージやPull Requestのレビュー補助など、ワークフローにAIエージェントを組み込む事例は今後も増加が見込まれます。
AIエージェントやLLMをアプリケーションに組み込む際のセキュリティリスクについては、弊社でも2025年の記事を公開しています11。GitHub Actions上のAIエージェントにおいても、これらのリスクは例外ではありません。
AIエージェントをワークフロー上で使用する場合、検討すべき権限は多岐にわたります。具体的には以下の3つの権限が存在します。
- ワークフロー自体の権限:
GITHUB_TOKENのpermissionsやsecretsへのアクセス範囲 - AIエージェントのツール実行権限: Bash実行、ファイル読み書き、GitHub APIの呼び出しなど
- AIエージェントの起動権限: 誰がAIエージェントを起動できるか
これらの権限に対する過剰な権限付与が組み合わさると、外部から攻撃されうる状況が発生します。事例分析で取り上げたclineの侵害では、2と3が広く設定されていました。
特に注意が必要なのは、AIエージェントに対するprompt injectionです。issuesやissue_commentトリガーで動作するAIエージェントの場合、IssueのタイトルやコメントがAIエージェントへの入力となります。パブリックリポジトリでは誰でもIssueを作成できるため、攻撃者はIssueのタイトルや本文に悪意ある指示を埋め込むことが可能です。AIエージェントがBashの実行権限を持っていれば、prompt injectionを通じてrunner上で任意のコマンドを実行させることができます。
ここで「Bashの実行権限を与えなければ安全か」という疑問が生じるかもしれません。しかし、AIエージェントがGitHub APIを呼び出す機能を持っていて、かつワークフローのGITHUB_TOKENがcontents: write権限を持っていれば、Bashを使わずともリポジトリへのファイル書き込みやブランチの操作が可能です。
加えて、clineの事例で見たように、AIエージェント経由の攻撃は他の権限昇格手法と組み合わされることがあります。clineの場合はキャッシュポイズニングを経由して権限が高いワークフローにアクセスし、最終的にパッケージレジストリのトークンが窃取されました。
AIエージェントに与えた権限が直接的には限定的であっても、runner上でコマンドを実行できる時点で、別記事で紹介する各種権限昇格の起点となりえます。
外部actionのバージョンをタグで指定することによる汚染
GitHub Actionsでは、外部Actionをusesフィールドで指定する際に、タグ(v1やv1.2.3など)を使ってバージョンを指定するのが一般的です。しかし、Gitのタグは作成後でも削除・再作成・修正が可能であり、同じタグ名で異なるcommitを指すように変更できます。
# タグ指定の例(改竄リスクあり) - uses: example/action@v1
この場合、example/actionのリポジトリでv1タグが別のcommitに付け替えられると、ワークフローは変更後のcommitを参照して実行します。
タグの付け替えは、リポジトリへの書き込み権限があればgitコマンドで実行可能です。事例分析で紹介したtj-actions/changed-filesの侵害では、窃取されたPATを使ってタグが悪意あるcommitに付け替えられました。trivy-actionの侵害でも同様にタグの改竄が行われています。
タグ指定にはv1のようなメジャーバージョンタグとv1.2.3のような完全バージョンタグがありますが、いずれもGitのタグである以上、改竄のリスクは同じです。
また、GitHubの仕様上、forkリポジトリで作成されたcommitは、親リポジトリのcommit hashとしても参照可能です。削除されたPull RequestやforkのcommitもGitHub上では参照可能な状態が継続します。この挙動はImposter Commitsと呼ばれます。
これを悪用すると、攻撃者はforkリポジトリで悪意あるcommitを作成し、そのcommit hashを親リポジトリのcommitであるかのように参照させることができます。
この挙動は以下のようなURLにアクセスすると確認可能です。その際に、画面に「このcommitはこのリポジトリに属していない可能性がある」と表示されます。
https://github.com/<fork元の名前>/<リポジトリ名>/commit/<forkのcommit hash>

この挙動が実際に悪用された事例として、trivyの2度目の侵害では、Imposter Commitsを使ってaquasecurity/trivy-actionのタグが悪意あるcommitに付け替えられました。
パブリックリポジトリでSelf-hosted runnerを使用している
パブリックリポジトリでSelf-hosted runnerを使用している場合、fork元の外部ユーザーがそのrunner上でコードを実行できる可能性があります。
前述のとおり、pull_requestトリガーはPull Requestのコンテキストで動作します。forkからPull Requestが送信された場合、pull_requestトリガーのワークフロー自体を変更するPull Requestであれば、変更後のワークフローが実行されます。この仕様により、攻撃者はワークフローのruns-onを被害者のSelf-hosted runnerに変更した上で、悪意あるstepを追加することで、被害者のSelf-hosted runner上で任意のコードを実行できます。
前述の通り、forkからのpull_requestトリガーでは書き込み権限が制限され、secretsも参照できないようになっています。しかし、Self-hosted runnerの使用は制限されません。
Self-hosted runnerが社内ネットワーク上に配置されている場合、攻撃シナリオは以下のように展開しえます。
- 攻撃者がパブリックリポジトリにforkからPull Requestを送信する
pull_requestトリガーのワークフローを変更し、runs-onをSelf-hosted runnerに指定する- Self-hosted runner上で任意コマンドが実行される
- runnerが社内ネットワークに接続されている場合、内部のサービスやAPIへのアクセスが可能になる
- 永続化のためにrunnerのファイルシステムにバックドアが設置される可能性がある
GitHub-hosted runnerは一時的なものであるため、実行後に環境が破棄されます。一方、Self-hosted runnerはデフォルトでは永続的に残るため、前回の実行で残された認証情報やファイルが次の実行にも残り続けるリスクがあります。GitHubの公式ドキュメントでも、パブリックリポジトリでのSelf-hosted runnerの使用は推奨されていません12。
なお、この攻撃の前提条件は、リポジトリにSelf-hosted runnerが登録されていることです。Self-hosted runnerは登録時にデフォルトでself-hostedラベルが付与されるため、ワークフローからruns-on: self-hostedと指定するだけでジョブがSelf-hosted runner上で実行されます。
--no-default-labelsフラグを指定して登録した場合はカスタムラベルを付与することが可能です。カスタムラベルは、デフォルトラベルと累積的に働くのでruns-on: self-hostedだけではルーティングされません13。ただし、カスタムラベルが推測可能であれば同様の攻撃が成立します。
初期アクセスを防ぐ
ここまで紹介した初期アクセスの手法に対して、実践できる対策を解説していきます。まずは対策の全体像を示します。詳細はそれぞれの章をご確認ください。
pull_request_targetトリガーでPull Request送信者のコードをcheckoutしない。- どうしても必要な場合は
pull_requestトリガーへの変更を検討するか、ジョブ分離・権限の最小化・artifactによる受け渡しでリスクを軽減する
- どうしても必要な場合は
- 外部コンテキスト(
${{ }})はrunフィールドで直接展開せず、中間環境変数に格納してからシェル変数として参照する。- 運用負荷を下げるため、外部コンテキストはすべて中間環境変数経由で使用するとルール化するのが望ましい
- 外部ワークフローから渡されたartifactは信頼せず検証して使用する。
- ダウンロード時には
pathを必ず指定し、値の形式チェックやGITHUB_OUTPUTへのデリミタ構文の使用でインジェクションを防ぐ
- ダウンロード時には
- AIエージェントに与えるツール権限・実行者の範囲・ワークフロー権限をそれぞれ最小にする。
Bashの無制限許可や実行者の無制限許可(例:allowed_non_write_users: "*")は避け、必要な操作だけを個別に許可する
- 外部Actionはタグではなくcommit hashでピン留めする。
- リポジトリやOrganizationの設定でcommit hashの使用を強制することも可能
基本的にPull Requestのcommitはcheckoutしない
パブリックリポジトリでは、誰でもfork経由でPull Requestを送信できます。攻撃者もPull Requestの内容に自由に攻撃コードを含めることが可能であり、checkoutした時点で後続のstepすべてに攻撃者のコードが影響します。
「使用するファイルを限定したり、都度検証すれば問題ない」と考えるかもしれません。しかし、checkoutされたコードからコマンド実行に至るシナリオは想像以上に多岐にわたります。LOTP(Living Off the Pipeline)14というプロジェクトでは、npm installのpreinstallスクリプト、Makefileの暗黙的ルール、.bashrcの読み込みなど、checkoutされたファイルから間接的にコマンドを実行させる手法が多数収集されています。
したがって、pull_request_targetトリガーでPull Request送信者のコードをcheckoutしないことが最も確実な対策です。
どうしてもcheckoutが必要な場合は、まずpull_requestトリガーへの変更を検討してください。forkからのpull_requestトリガーは書き込み権限やsecretsへのアクセスが制限されているため、pull_request_targetトリガーと比べて考慮すべきリスクが少なくなります。
pull_request_targetトリガーでcheckout処理を使わざるを得ない場合は、 以下の構成でリスクを軽減できます。
- ジョブを分離する:checkout処理を行うジョブと、secretsを使用するジョブを別にする
- 権限を最小にする:checkoutするジョブでは
permissions: {}を設定し、GITHUB_TOKENの権限を空にする - 結果をartifactで受け渡す:checkoutしたジョブの処理結果をartifactとして出力し、後続のジョブで検証した上で使用する

以下はpull_request_targetトリガーにおける安全な構成のサンプルです。
on: pull_request_target
jobs:
# ジョブ1: 権限なしでcheckout・処理を行う
build:
runs-on: ubuntu-latest
permissions: {} # 権限を空にする
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm test
- uses: actions/upload-artifact@v4
with:
name: test-results
path: results/
# ジョブ2: artifactを検証してから使用する
process-results:
needs: build
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/download-artifact@v4
with:
name: test-results
path: ./results/
# artifactの内容を検証した上で使用する
ただし、この構成にも制約があります。pull_request_targetトリガーはデフォルトブランチで動作する関係上、permissions: {}を設定してもキャッシュ領域への書き込み権限は残るため、キャッシュポイズニングの可能性は排除できません。artifactの検証も必須であり、運用負荷は高くなります。可能であれば、Pull Requestのコードをcheckoutしない構成を検討するのが安全でしょう。
外部コンテキストは中間環境変数に格納して使用する
外部コンテキスト(${{ }})をrunフィールドで直接参照すると、Script Injectionの脆弱性が生じます。中間環境変数に格納してからシェル変数として参照することが、現状で確実なScript Injection対策です。
jobs:
test_job:
runs-on: ubuntu-latest
steps:
- name: Print issue title
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
run: echo "Processing issue: $ISSUE_TITLE"
envフィールドで環境変数に格納した後、シェル変数($ISSUE_TITLE)として参照することで、シェルのクォートルールに従って安全に展開されます。この方法はGitHub公式ドキュメントでも推奨されています15。
注意点として、runフィールドだけでなく、actions/github-scriptのscriptフィールドなど、外部コンテキストが展開されるすべての箇所が対象です。また、github.event.issue.title以外にも、github.event.pull_request.title、github.event.pull_request.body、github.event.pull_request.head.ref(ブランチ名)、github.event.comment.bodyなど、外部ユーザーが制御可能なコンテキストは多数存在します。
毎回判断すると運用負荷が上がるため、「外部コンテキストは全て中間環境変数に入れて使用する」とルールを決めてしまうと良いでしょう。
外部ワークフローから渡されたartifactは検証して使用する
前述で「artifactを検証してから使用する」と記載しましたが、本セクションはその具体的な内容です。
まず、artifactをダウンロードする際は、必ずpathを指定しましょう。pathが未指定の場合、カレントディレクトリに展開されるため、actions/checkoutで取得したファイルが上書きされる可能性があります。
以下はpathを指定した場合のサンプルコードです。
steps:
- uses: actions/download-artifact@v4
with:
name: my-artifact
path: ./artifact-output/ # 展開先を明示する
artifactの内容をrunフィールドで使用する場合は、Script Injection対策と同様に中間環境変数に格納してから参照してください。PR番号やステータスのように形式が固定された値については、正規表現などで検証してから使用しましょう。
GITHUB_OUTPUTに出力する際は、改行インジェクションを防ぐためにデリミタ構文16を使用してください。GITHUB_OUTPUTはkey=valueの形式で1行ずつ値を書き込みますが、valueに改行が含まれていると、改行以降が別のkey-valueペアとして解釈されてしまいます。たとえば、artifactの内容がdata\nisAdmin=trueだった場合、GITHUB_OUTPUTには以下のように書き込まれます。
test=data isAdmin=true
この結果、攻撃者が任意のoutput変数を注入できてしまいます。さらに、同名のキーが複数存在する場合は後に書かれた値が優先されるため、事前にisAdmin=falseを設定していても上書きされます。デリミタ構文を使えば値全体が1つのoutputとして扱われるため、この改行インジェクションを防ぐことができます。
また、デリミタには予測不可能な値を設定することで、artifact経由で注入される値によるインジェクションを防ぎます。
- name: Set output safely
run: |
DELIMITER=$(openssl rand -hex 16)
echo "result<<${DELIMITER}" >> "$GITHUB_OUTPUT"
cat ./artifact-output/result.txt >> "$GITHUB_OUTPUT"
echo "${DELIMITER}" >> "$GITHUB_OUTPUT"
その他にも、以下の点に注意が必要です。
- ビルド済みのバイナリやスクリプトをartifactから実行するといったリスクが高い処理を行う場合は、
workflow_runトリガーでbranchesを指定して実行ブランチを限定する。また、イベント発生元が想定のものであるかどうかを検証する。 - artifactの内容をPull Requestのコメントに出力する場合は、Markdownインジェクションを防ぐためにサニタイズする
AIエージェントに与える権限は最小にする
AIエージェントをワークフロー上で使用する場合、脆弱性の項で解説した3つの権限(実行者の範囲、ツール権限、ワークフロー権限)それぞれで最小権限を適用する必要があります。
現時点で広く使われているanthropics/claude-code-actionを例に、具体的な設定を見ていきます。
実行者の範囲を制限する
claude-code-actionはデフォルトでリポジトリのwrite権限を持つユーザーのみがトリガーできます。しかし、allowed_bots: "*"を設定すると全てのGitHub Appからのトリガーを受け入れ、allowed_non_write_users: "*"を設定すると外部ユーザーを含む全員がトリガー可能になります。
clineの侵害ではallowed_non_write_users: "*"が設定されていたことで、攻撃者がIssueのタイトルにprompt injectionを仕込み、AIエージェント経由で任意コード実行を獲得しました。allowed_botsやallowed_non_write_usersに*を設定するのは避け、信頼するBot名やユーザー名を明示的に指定しましょう。
ツール権限を制限する
claude-code-actionのtag mode(@claudeメンション)では、デフォルトでファイルの読み取り、Git操作(add/commit/push)、CI結果の取得が許可されています。任意のBashコマンドはデフォルトでは許可されていません。
claude_argsの--allowedToolsで追加のツールを許可する場合は、必要な操作だけを個別に指定します。Bashを無制限に許可すると、prompt injection経由でrunner上の任意コマンドが実行可能になるため、避けるべきです。
# 危険な設定 claude_args: "--allowedTools Bash" # 安全な設定(必要なコマンドだけ許可) claude_args: "--allowedTools 'Bash(npm run test:*)'"
ワークフロー権限を最小にする
ツール権限を最小限にした場合であっても、claude-code-actionはMCPサーバー経由でGitHub APIを直接呼び出す機能を持っているため、トークンにcontents: write権限があればリポジトリの内容を書き換えられます。ツール権限とワークフロー権限の両方を絞ることが重要です。
AIエージェントを使用するワークフローでは、permissionsを必要最小限に設定します。Issueのトリアージだけであればissues: writeのみで十分であり、contents: writeは不要なケースがほとんどです。
ワークフロー上で動作するAIエージェントを取り巻く状況は日々進歩しており、GitHub公式もGitHub Agentic Workflowsを発表しています17。
GitHub Agentic Workflowsはセキュアアーキテクチャを指向しており、特にAIエージェントを隔離環境で実行する点が特徴的です。ワークフロー上でのAIエージェント活用は急速に進化しているため、使用するActionの最新のセキュリティドキュメントを定期的に確認することをお勧めします。
外部Actionを使用する際はcommit hashでピン留めする
外部Actionを使用する際は、commit hashでピン留めすることでリスクの軽減が可能です。
以下に脆弱になりうる設定と安全な設定を示します。
# 脆弱な例: タグ指定(付け替え可能) - uses: actions/checkout@v4 # 安全な例: commit hashでピン留め - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
commit hashはコード内容と一意に紐づくため、同じhashのまま参照先のコードが変わることはありません。
また、リポジトリやOrganizationでRequire actions to be pinned to a full-length commit SHAを有効化すれば、commit hashの使用を強制できます。
現状では、commit hashでピン留めしたとしても完全な対策にはなりません。
外部Actionが推移的に別の外部Actionを呼び出している場合、呼び出し先のActionがピン留めされていなければ汚染の可能性は残ります。
この問題を受け、GitHub公式はロードマップでworkflow-level dependency lockingを発表しました18。
dependenciesフィールドを設定することで、Go言語のgo.mod + go.sumと同様の仕組みで依存関係を固定できる機能です。
将来的にこの機能がリリースされれば、外部Actionの汚染リスクは大幅に軽減されると考えられます。

コラム:依存関係の汚染による初期アクセスについて
これまでにGitHub Actionsに対する初期アクセスの手法と対策を紹介してきました。紹介した手法以外にも、近年特に狙われている攻撃経路があります。それが「依存関係の汚染」です。
依存関係の汚染とは、パッケージレジストリ(npm、PyPIなど)上に悪意あるパッケージを公開し、開発者やCI/CDパイプラインにインストールさせる攻撃手法の総称です。typosquatting(正規パッケージに酷似した名前での公開)やdependency confusion(内部パッケージ名と同名のパッケージをパブリックレジストリに公開する手法)などが代表的な手口として知られています。
本記事で取り上げた侵害事例を振り返ると、依存関係の汚染はGitHub Actionsへの攻撃と密接に関わっていることがわかります。
本稿で紹介したnxの侵害では、GitHub Actionsから窃取したNPM_TOKENを使って悪意あるパッケージがnpmに公開されました。clineの侵害でも最終的にnpmとVS Code Marketplaceへの不正な公開が行われ、trivyの事例では改竄されたバイナリが公式リリースとして配布されています。これらの事例では、GitHub Actionsの侵害はあくまで手段であり、真の目的はパッケージレジストリへの悪意あるコードの配布でした。攻撃者がCI/CDパイプラインを狙う動機の一つが、まさにこの依存関係の汚染にあるといえるでしょう。
一方で、依存関係の汚染が初期アクセスとなるケースもあります。
(これはMITREではT1195/001のSupply Chain Compromise: Compromise Software Dependencies and Development Toolsとされています)
nxの侵害では、公開された悪意あるパッケージにインストール先の環境からGitHub tokenやnpm tokenを窃取するコードが含まれており、2次被害、3次被害へと繋がりました。
この連鎖の中では、悪意あるnxパッケージをインストールした開発者やCI/CD環境にとって、依存関係の汚染こそが初期アクセスの手段です。tj-actionsの侵害でも、改竄されたActionがワークフローのsecretsをビルドログに出力し、パブリックリポジトリではそのログが公開状態でした。窃取したsecretsを使えば、さらに別のリポジトリやパッケージレジストリへの攻撃が可能になります。
このように、GitHub Actionsの侵害によって公開された悪意あるパッケージが、次の被害者にとっての初期アクセスとなり、攻撃が連鎖していく構造が近年の特徴です。
しかし、依存関係の汚染はパッケージレジストリを経由して入り込むため、ワークフローの設定をどれだけ堅牢にしても防ぐことができません。
弊社GMO Flatt Securityでは、こうしたソフトウェアサプライチェーンの脅威に対応するプロダクトとして「Takumi Guard」を開発・提供しています19。
GitHub Actionsのセキュリティは、ワークフローの構成だけで完結するものではありません。依存関係を含むソフトウェアサプライチェーン全体を視野に入れた対策が、今後ますます重要になっていきます。
終わりに
本稿では、GitHub Actionsに対する近年の侵害事例を振り返りつつ、初期アクセスに使用される攻撃手法とその対策を中心に解説しました。攻撃の入口は多様ですが、本稿で紹介したとおり技術的な対策は一定存在します。一方で、これらをすべてのワークフローに対して人手で継続的に適用・維持していくのは容易ではありません。
次回の記事では、初期アクセスを獲得された後の権限昇格の手法と、侵害時の影響を最小化するための対策について解説する予定です。本シリーズが、皆さんのGitHub Actions環境のセキュリティを見直す一助となれば幸いです。
ここまでお読みいただきありがとうございました。
- https://github.com/tj-actions/changed-files↩
- https://github.com/nrwl/nx↩
- https://github.com/aquasecurity/trivy↩
- https://github.com/cline/cline↩
- https://www.stepsecurity.io/blog/prevent-ultralytics-style-ci-cd-security-attacks-with-network-security-controls↩
- https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c↩
- https://github.com/tj-actions/changed-files↩
- https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised↩
- https://www.stepsecurity.io/blog/trivy-compromised-a-second-time---malicious-v0-69-4-release↩
- https://adnanthekhan.com/posts/clinejection/↩
- https://blog.flatt.tech/entry/llm_application_security↩
- https://docs.github.com/ja/actions/reference/security/secure-use#hardening-for-self-hosted-runners↩
- https://docs.github.com/ja/actions/how-tos/manage-runners/self-hosted-runners/use-in-a-workflow#using-custom-labels-to-route-jobs↩
- https://boostsecurityio.github.io/lotp/↩
- https://docs.github.com/ja/actions/reference/security/secure-use#using-an-intermediate-environment-variable↩
- https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings↩
- https://github.github.com/gh-aw/↩
- https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/↩
- https://flatt.tech/takumi/features/guard↩