GitHub Actions の式構文はスクリプトインジェクションの温床になる
はじめに
GitHub Actions の actions/github-script や run: ブロックで ${{ }} を使うとき、多くの開発者がセキュリティリスクを見落としています。PR の自動タグ付けやリリースノート生成など、よくある CI ワークフローにもこのリスクは潜んでいます。
筆者が Go で書かれたコード生成ツールの CI ワークフローをセキュリティレビューした際、この問題を実際に発見しました。本記事では攻撃手法と対策を具体的なコード例とともに解説します。
${{ }} の展開はテンプレートエンジンそのもの
GitHub Actions の式展開 ${{ }} は、ワークフロー実行前にテキスト置換されます。
if: 条件で使う場合は式として評価されるため安全ですが、run: ブロックや script: の中で使うと、展開結果がそのままシェルコマンドや JavaScript として実行されます。構造的にはテンプレートインジェクションと同じです。
攻撃シナリオ1: github-script での注入
以下は PR のマージ後にタグ付けを行うワークフローの一部です。
# 危険なコード例
- uses: actions/github-script@v7
with:
script: |
const sha = '${{ steps.pr.outputs.merge_sha }}';
const tag = '${{ steps.tag.outputs.new_tag }}';
const success = '${{ job.status }}' === 'success';
一見問題なさそうに見えます。しかし merge_sha の値が攻撃者によって操作された場合を考えてみましょう。
merge_sha が '; exec('malicious code'); ' だった場合、展開後の JavaScript はこうなります。
const sha = "";
exec("malicious code");
("");
const tag = "v1.0.0";
const success = "success" === "success";
引用符が閉じられ、任意の JavaScript コードが実行されます。攻撃者が PR タイトルやブランチ名を操作できる場合、github.event.pull_request.title なども同様に危険です。
攻撃シナリオ2: run ブロックでのシェルインジェクション
run: ブロックでも同じ問題が起きます。
# 危険なコード例
- run: |
echo "Processing PR: ${{ github.event.pull_request.title }}"
git tag "${{ steps.tag.outputs.new_tag }}"
PR タイトルが "; rm -rf / # の場合、展開後のシェルコマンドはこうなります。
echo "Processing PR: "; rm -rf / #"
git tag "v1.0.0"
echo が空文字列で終了し、rm -rf / が実行され、# 以降はコメントとして無視されます。PR タイトルは PR 作成者が自由に設定できるため、外部からの入力として扱う必要があります。
修正方法: 環境変数を経由する
対策はシンプルです。${{ }} の値を環境変数に入れ、コードからは環境変数を参照します。
github-script の場合
# Before(脆弱)
- uses: actions/github-script@v7
with:
script: |
const sha = '${{ steps.pr.outputs.merge_sha }}';
const tag = '${{ steps.tag.outputs.new_tag }}';
const success = '${{ job.status }}' === 'success';
# After(安全)
- uses: actions/github-script@v7
env:
MERGE_SHA: ${{ steps.pr.outputs.merge_sha }}
NEW_TAG: ${{ steps.tag.outputs.new_tag }}
JOB_STATUS: ${{ job.status }}
with:
script: |
const sha = process.env.MERGE_SHA || '';
const tag = process.env.NEW_TAG || '';
const success = process.env.JOB_STATUS === 'success';
run ブロックの場合
# Before(脆弱)
- run: |
echo "Processing PR: ${{ github.event.pull_request.title }}"
# After(安全)
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "Processing PR: ${PR_TITLE}"
環境変数を経由すると、値は「データ」として扱われ、「コード」として解釈されません。SQL インジェクション対策と同じ原則、データとコードの分離です。
${{ }} を使って良い場所・悪い場所
| 場所 | 安全性 | 理由 |
|---|---|---|
if: 条件 |
安全 | 式として評価され、コード実行されない |
env: の値 |
安全 | 環境変数の値として設定されるだけ |
with: の入力 |
条件付き | アクション側の処理に依存する |
run: の中身 |
危険 | シェルコマンドとして実行される |
script: の中身 |
危険 | JavaScript として実行される |
判断基準は明確で、展開先が「コード実行コンテキスト」かどうかです。
自分のワークフローをチェックする方法
既存のワークフローに同様の問題がないか確認するには、リポジトリ内で以下のように検索します。
# run/script ブロック付近の ${{ }} をコンテキスト付きで確認
rg -n -C3 '\$\{\{' .github/workflows/*.yml
if: や env: 以外の場所で ${{ }} が使われている箇所が見つかったら、環境変数への置き換えを検討してください。
手動検索に加えて、静的解析ツールを使う方法もあります。actionlint はワークフローの構文チェックに加え、${{ }} の危険な使用箇所を検出できます。zizmor はスクリプトインジェクションを含むセキュリティリスクの検出に特化したツールです。どちらも CI に組み込めば、レビュー前に問題を自動検出できます。
まとめ
-
${{ }}はテンプレート置換であり、展開先のコード実行コンテキストでは注入リスクになる - 環境変数を経由することで「データ」と「コード」を分離できる
- CI ワークフローのセキュリティレビューでは
${{ }}の使用箇所を最初にチェックする - 特に
github.event.pull_request.titleやgithub.event.pull_request.bodyなど、外部ユーザーが操作可能な値には注意が必要です
参考: Understanding the risk of script injections(GitHub 公式ドキュメント)
Discussion