GitHub Actionsのワークフローに潜む罠、気づいてた?スクリプトインジェクションの実例と対策

目次

そのワークフロー、本当に安全ですか?

GitHub Actions を使っている開発者なら、こんなワークフローを書いたことがあるはずです。

on:
  pull_request:

jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "PR title is ${{ github.event.pull_request.title }}"
      - run: echo "PR Head Ref is ${{ github.head_ref }}"

シンプルで問題なさそうに見えますよね。でも実はこれ、外部の攻撃者が任意のコードをワークフロー上で実行できてしまう脆弱なコードです。

この記事では、スクリプトインジェクションの仕組みを実例で解説し、正しい対策方法までを丁寧に説明します。「なんとなく動いてるけど安全か不安…」という方は、ぜひ最後まで読んでみてください。

スクリプトインジェクションとは何か

GitHub Actions の run ステップ内で ${{ }} 式を使うと、ワークフローが実行される前にその値が文字列としてそのまま展開されます。

たとえば PR タイトルが fix: typo であれば、以下のように展開されて実行されます。

# ワークフローの定義
- run: echo "PR title is ${{ github.event.pull_request.title }}"

# 実行時に展開されるスクリプト
- run: echo "PR title is fix: typo"

通常はこれで問題ありません。問題は、ユーザーが自由に入力できる値(PRタイトル、ブランチ名、Issueタイトルなど)が含まれる場合です。悪意のある値を入力されると、シェルコマンドとして解釈されてしまいます。

実際にやってみる:PRタイトルで任意コード実行

以下のワークフローを例に、実際にインジェクションを試してみます。

on:
  pull_request:

jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "PR title is ${{ github.event.pull_request.title }}"

ここで、PR のタイトルを "; echo INJECTED" という文字列にして Pull Request を作成します。すると、ワークフロー実行時に以下のように展開されます。

# 展開後のスクリプト
- run: echo "PR title is "; echo INJECTED""

# シェルはこう解釈する
# 1. echo "PR title is " を実行
# 2. echo INJECTED"" を実行 → INJECTED が出力される

実行ログには INJECTED という文字列が出力されます。任意のコードを実行することに成功してしまいました。

今回は echo INJECTED という無害なコマンドで実証しましたが、同じ方法で シークレットの抜き取りや外部サーバーへのデータ送信も可能です。

ブランチ名でもできてしまう

「PRタイトルは自由に書けるからわかるけど、ブランチ名は大丈夫では?」と思った方、甘いです。

on:
  pull_request:

jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      - run: echo "PR Head Ref is ${{ github.head_ref }}"

ブランチ名にはスペースを入れることができません。しかしシェルには ${IFS}(Internal Field Separator)という単語分割文字を表す特殊変数があり、スペースの代わりに使えます。

ブランチ名を main";echo${IFS}INJECTED" として PR を作成すると、以下のように展開されます。

# 展開後のスクリプト
- run: echo "PR Head Ref is main";echo${IFS}INJECTED""

# シェルはこう解釈する
# 1. echo "PR Head Ref is main" を実行
# 2. echo INJECTED"" を実行 → INJECTED が出力される

こちらも見事にインジェクション成功です。PRタイトル・ブランチ名・Issueタイトルなど、ユーザーが自由に入力できる値はすべて危険と考えてください。

「プライベートリポジトリなら安全」は誤解

「パブリックリポジトリは外部から PR を作られるから危険だけど、プライベートなら大丈夫」と思っていませんか?残念ながら、それは誤解です。

たとえば、以下のようなシナリオが現実に起こりえます。

  1. プライベートリポジトリに GitHub App(Contents: Write + Pull Requests: Write 権限)をインストールしている
  2. 攻撃者がこの GitHub App を侵害・乗っ取る
  3. GitHub App を使ってプライベートリポジトリに悪意ある PR を作成する
  4. スクリプトインジェクション成功

通常、Workflows: Write 権限がなければワークフロー定義は書き換えられません。しかしスクリプトインジェクションを利用すれば、それより低い権限でも GitHub Actions 上で任意コードを実行できてしまいます。

「プライベートだから対策しなくていい」は「プライベートリポジトリなら AWS アクセスキーをコミットしても大丈夫」と同じ理論です。やらない理由がないので、きちんと対策しましょう。

正しい対策:環境変数を経由して渡す

対策はシンプルです。run の中で ${{ }} を直接使わず、必ず環境変数を経由して渡すようにしてください。

on:
  pull_request:

jobs:
  example:
    runs-on: ubuntu-latest
    steps:
      # ❌ 危険:${{}} を run 内で直接使用
      - run: echo "PR title is ${{ github.event.pull_request.title }}"

      # ✅ 安全:環境変数を経由して参照
      - run: echo "PR title is ${PR_TITLE}"
        env:
          PR_TITLE: ${{ github.event.pull_request.title }}

値をシェルの環境変数として扱うことで、シェルへの展開時にコマンドとして解釈されることを防げます。先ほどの "; echo INJECTED" というタイトルもそのまま文字列として出力されるだけになります。

よくある勘違い:${{ env.* }} はNG

「環境変数経由ってことは ${{ env.PR_TITLE }} を使えばいいの?」と思った方、それは同じ問題が発生します。

steps:
  # ❌ これも危険:${{ env.* }} も実行時に展開されるのでインジェクションされる
  - run: echo "PR title is ${{ env.PR_TITLE }}"
    env:
      PR_TITLE: ${{ github.event.pull_request.title }}

  # ✅ これが正解:シェルの環境変数 ${PR_TITLE} を使う
  - run: echo "PR title is ${PR_TITLE}"
    env:
      PR_TITLE: ${{ github.event.pull_request.title }}

${{ env.* }} も結局は実行前に文字列展開されてしまいます。必ず ${変数名} というシェルの環境変数の形で参照してください。

デフォルト環境変数の活用

GitHub Actions にはデフォルトで用意されている環境変数も多くあります。たとえば github.head_ref に対応する GITHUB_HEAD_REF が最初から使えるため、自分で環境変数を定義する必要すらありません。

steps:
  # ❌ 危険
  - run: echo "PR Head Ref is ${{ github.head_ref }}"

  # ✅ 安全:デフォルト環境変数をそのまま使う
  - run: echo "PR Head Ref is ${GITHUB_HEAD_REF}"

よく使うデフォルト環境変数の例です。

コンテキスト式 デフォルト環境変数
${{ github.head_ref }} ${GITHUB_HEAD_REF}
${{ github.sha }} ${GITHUB_SHA}
${{ github.repository }} ${GITHUB_REPOSITORY}
${{ github.actor }} ${GITHUB_ACTOR}
${{ github.ref }} ${GITHUB_REF}

一覧は GitHub 公式ドキュメント で確認できます。

自動検出ツール(actionlint / zizmor)

既存のワークフローに問題がないか手動で確認するのは大変です。以下の静的解析ツールを使えば、スクリプトインジェクションの脆弱性を自動で検出できます。

actionlint

GitHub Actions ワークフロー専用の静的解析ツールです。スクリプトインジェクション以外にも多くの問題を検出してくれます。

# インストール(macOS)
brew install actionlint

# 実行
actionlint

問題のあるワークフローに対して、以下のような出力が得られます。

.github/workflows/title.yml:8:36: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. [expression]

zizmor

GitHub Actions のセキュリティに特化した解析ツールです。自動修正機能も持っています。

# インストール(macOS)
brew install zizmor

# 実行
zizmor .github/workflows/

検出結果には問題箇所と修正方法が明示されるため、初めて導入する場合でも対処しやすいです。CI に組み込んでおくと、問題のあるワークフローが混入するのを自動で防げます。

GitHub
GitHub - rhysd/actionlint: :octocat: Static checker for GitHub Actions workflow files :octocat: Static checker for GitHub Actions workflow files - rhysd/actionlint

まとめ

GitHub Actions のスクリプトインジェクション対策をまとめます。

  • run の中で ${{ }}直接使わない
  • ユーザーが入力できる値(PRタイトル・ブランチ名・Issueタイトルなど)は特に危険
  • 値は必ずシェルの環境変数(${変数名})を経由して参照する
  • ${{ env.* }}run 内で使うのも同様にNG
  • デフォルト環境変数(GITHUB_HEAD_REF 等)が使える場合はそちらを活用
  • プライベートリポジトリでも対策は必須
  • actionlint・zizmor で既存ワークフローを自動チェック

「これくらい大丈夫だろう」という思い込みが、シークレットやAPIキーの流出につながります。ルールはシンプルなので、今すぐワークフローを見直してみてください。

GitHub Actionsのセキュリティをさらに強化する関連記事

ワークフローのセキュリティ対策を習得したら、開発環境全体のセキュリティも合わせて強化しましょう:

セキュリティ・認証まわり

デプロイ・CI/CDまわり

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次