Issue や PR のタイトルに仕込まれた $() で CI の環境変数を抜く攻撃:通知・ラベル付けワークフローを点検する
外部の人が書ける Issue・PR・コメントの文字列を run: に ${{ }} で直接埋め込むと、二重引用符の中でも $() が実行される。2026-09-01 に 61 リポジトリへ 83 件の仕込み投稿が送られた実例があり、値は env: 経由で渡すのが基本。
実際に起きたこと(Radware の調査)
- 2026-09-01 の約 43 分で 77 件、6 時間余り後に 6 件の投稿。仕込み先はタイトル 31 件、本文 17 件、コメント 35 件。agno、ToolJet、trivy、mlflow、Podman などが標的になった。
- 中身は「環境変数を Base64 にして攻撃者のサーバーへ HTTP 送信する」コマンド置換。承認やマージは要らず、投稿されるだけでワークフローが動く。
- 実行が確認されたのは、PR タイトルの形式チェック(PX4-Autopilot)と、Issue を Discord に通知する処理(workout-cool)の 2 件。どちらも「ビルドではない小さな補助ワークフロー」だった。
- 投稿元の一部は開発歴のある既存アカウントで、乗っ取られた可能性がある。投稿者の見た目では判断できない。
- 別のプロジェクトでは、認証情報にアクセスできる AI のコメントレビューエージェントが投稿を読みうる状態だった。
直し方
悪い例(タイトルがそのままシェルスクリプトになる):
yaml
- run: echo "New issue: ${{ github.event.issue.title }}"良い例(値は環境変数として渡し、シェル側で文字列として扱う):
yaml
- env:
TITLE: ${{ github.event.issue.title }}
run: printf 'New issue: %s\n' "$TITLE"- 読み出した値を
evalやbash -cに渡したり、別のコマンド文字列に組み込んだりしない。 permissions:を最小にし、外部投稿で起動するワークフロー(issues、issue_comment、pull_request_targetなど)と、シークレットを使うジョブを分ける。- Discord・Slack の Webhook URL もシークレット。通知ワークフローで流出すると、なりすまし投稿に使われる。
点検のしかた
.github/workflows/で${{ github.event.を含むrun:行を探す(例:rg -n 'run:.*\$\{\{ *github\.event' .github/workflowsと、複数行のrun: |の中も確認)。title、body、head_ref、comment.bodyが要注意。- 静的解析ツール(zizmor、actionlint など)でテンプレートインジェクションを検出する。
- 実行痕跡(Bash の警告、想定外の外部通信)があれば、そのジョブから見えたシークレットを失効・再発行し、監査ログを確認する。
- AI レビューボットなど、外部投稿を読んで動く仕組みが持つトークンの権限も見直す。
関連
- GitHub Actions の保護強化後も残る「Pwn Request」の経路を潰す
- GitHub Actions / CI/CD ハードニングガイド
- メールアドレスもシェルに渡せば命令になる:GNU Savannah の GPG 鍵テストからの侵入