何が起きたか・対象
kptへ外部PRを送る投稿者が、ブランチ名にBashの構文を入れ、本家の解析Workflowで命令を実行させられる問題がありました。ブランチ名をログへ表示するrunスクリプトに、値をそのまま埋め込んだことが原因です。
説明用に名前がfix-$(id)なら、Bashへ渡るコードはecho "Head Ref: fix-$(id)"になります。Bashはダブルクォート内の$(id)もコマンド置換として評価します。本家のジョブ内でidを動かし、その実行ユーザーの識別情報を表示文字列へ入れます。これは無害な説明例で、観測された攻撃の名前ではありません。
同じ環境では後からSonarCloud用のSONAR_TOKENを使います。説明用の仮定として、先に動いた命令が後続スクリプトへトークンの外部送信処理を書き加えられると、ファイル変更はステップ終了後も残り、後でトークン付きで起動した際に送信処理も動きます。外部通信が届けば攻撃者が値を入手できます。対象ファイルの書き換えと後続の実行が条件で、原資料はその具体的な変更ファイルやこの仮定の実行成功まで示していません。識別子はGHSL-2026-207です。
報告・修正・公開の時系列
2026-09-08に非公開報告、09-30にPR #4780で修正、10-22に研究公開されました。検証コミットは6dfff203a6b49396add815a428c1d94370e9a3d6です。
正常ならどう動くはずだったか
PRのテストが成功すると、本家側のワークフローがSonarCloudのコード解析を行う構成でした。外部の投稿者が付けたブランチ名は、GitHub APIで取得してログへ文字として表示するためのデータです。解析用の権限を持つ本家のジョブで、その名前を命令として実行する必要はありません。 先行する外部PRのテストには、通常リポジトリのシークレットを渡しません。後続のworkflow_runで起動する本家の解析workflowは、解析用シークレットへアクセスできます。名前の出所が外部PRであることは、この移動によって変わりません。
技術的な原因
GitHub APIから取ったブランチ名(head.ref)をステップの出力へ移し、ログ表示用のrunスクリプトへ直接埋め込んでいました。GitHub Actionsは式(${{ ... }})を値へ置き換えてからシェル(Bash)を動かすので、ブランチ名に含まれるBash構文もコードの一部になります。
値の受け渡しを示す説明例です。APIが返すhead.refを、get_branchというidのステップがbranch_nameという出力名で公開するとします。別ステップのrun内をecho "Head Ref: ${{ steps.get_branch.outputs.branch_name }}"と書けば、GitHub Actionsが式を実際の名前へ置換した後、完成した文字列をBashへ渡します。ステップ出力は、後続ステップから名前を指定して参照する値です。このidと出力名は説明用で、製品の実設定を写したものではありません。
説明用にブランチ名がfix-$(id)なら、表示コードはecho "Head Ref: fix-$(id)"となります。$(...)はシェルのコマンド置換で、Bashはidを単なる文字列ではなくコマンドとして実行し、その出力を埋め込みます。idは実行ユーザーの識別情報を表示するOSコマンドです。ダブルクォート内でもこのコマンド置換は動きます。この名前は仕組みを示す無害な例で、観測された攻撃の名前ではありません。
攻撃が成立する条件と流れ
外部の投稿者がBashのコマンド置換を名前に含むブランチでPRを送り、Goのテストが成功して必要な成果物ができると、workflow_runを契機に本家の解析が始まります。ブランチ名のログ表示ステップで命令が動きます。構成によっては、外部PRのテストに管理者の承認も必要です。トークンに関する影響が成立するには、SONAR_TOKENが設定されていることも条件です。
通常、フォークからのpull_requestで起動するワークフローには、SONAR_TOKENなどのリポジトリのシークレットは渡されません。一方、workflow_runは本家リポジトリのデフォルトブランチのワークフローで実行されます。先行処理に渡されなかったシークレットにもアクセスできます。このワークフローで外部PRのデータを命令として実行すると、外部PRの処理には渡されないシークレットを使う環境で、その命令が動くことになります。
確認された事実と、想定される影響
ジョブのGITHUB_TOKENは読み取り専用でも、同じ実行環境の後続処理へSONAR_TOKENが渡されます。報告は、先に実行したコードが後続ステップで使うファイルを変更するなどして、トークンの取得・不正使用や解析結果の改変を可能にすると説明しています。注入の瞬間から全ての秘密が環境変数にあるという意味ではありません。
仕組みを説明するための仮定の例です。最初に、ブランチ名から実行された命令が、後の解析で起動するスクリプトを書き換えます。追加するのは、そのスクリプトへ渡されたトークンをHTTPリクエストに含め、攻撃者のサーバーへ送信する処理です。最初のステップが終わっても、同じ実行環境にある変更済みファイルは残ります。
後のステップがSONAR_TOKENを渡してそのスクリプトを起動すると、追加した送信処理も動きます。HTTPリクエストが攻撃者のサーバーへ届けば、攻撃者はそこでSonarCloudの認証トークンを入手します。成立には、そのファイルへの書き込み権限、後続での実際のロード、外部送信の経路が必要です。原資料は、具体的な変更ファイルや、この仮定の例の実行成功までは示していません。
取得したトークンでどのSonarCloud操作を行えるかは、その資格情報に与えられた権限で決まります。解析結果の改変も、先に動くコードが解析へ使うファイルなどを変更し、後の処理がその内容を採用する経路が前提です。実際の秘密窃取や全ての解析結果の改変が確認された事件報告としては扱いません。
実際の修正・対応方針
開示資料はPR #4780での修正を案内しています。ここでは変更差分の全体は直接確認していません。一般的な対策は、外部入力を参照する式(${{ ... }})をrunスクリプト内へ直接埋め込まず、ステップのenv(環境変数)に渡すことです。シェル内では、環境変数を"$BRANCH_NAME"のようにダブルクォートで囲んで参照します。変数の値に含まれる$(...)は、環境変数を参照するだけでは再解釈されず、単なる文字列データとして扱われます。変数の内容を後からevalなどで再評価してはいけません。
同じ説明例を環境変数で渡すなら、ステップのenv欄でBRANCH_NAME: ${{ steps.get_branch.outputs.branch_name }}と指定し、run欄はecho "Head Ref: $BRANCH_NAME"という固定コードにします。GitHub Actionsは名前をプロセスの環境変数へ渡し、Bashは固定コードを解析した後に変数の値を読みます。値の中の$(id)を新しいコードとして再解析しないため、直接埋め込む方式と結果が異なります。
利用者・運用者が確認すること
利用しているワークフローが修正後かを確認します。侵害の疑いがあれば、実行環境で変更されたスクリプトや設定ファイル、解析結果、トークンの使用履歴を調べ、必要な資格情報を失効させます。ログ表示のコードを直しても、すでに書き換えられたファイルは元に戻りません。侵害された実行環境は信頼できる状態から作り直します。
開発者への教訓(教材としての考察)
秘密を渡すステップだけを守っても、先に不正なコードが後続処理のスクリプトを書き換えられるなら、その処理が受け取る秘密を盗まれるおそれがあります。外部入力の命令実行だけでなく、変更できるファイル、後でそのファイルを読む処理、そこへ渡す秘密を順に確認します。
出典・確認範囲
- [1] External pull request branches can execute commands in kptGitHub Security Lab / 確認日 2026-10-29
- [2] Events that trigger workflows — workflow_runGitHub Docs / 確認日 2026-10-28
参照資料では実悪用や実際の被害件数を確認できません。未記載の実装差分は推測していません。