はじめに
こんにちは。バックエンドエンジニアのよしかわです。本記事では GitHub Actions のワークフローを少し安全に書くコツを一つご紹介いたします。
この記事はエモーションテック Advent Calendar 2024の10日目の記事です。
脆弱性を含むワークフローの例
今回取り上げるのはスクリプトインジェクション対策です。例として公式ドキュメントで脆弱性を含むとして挙げられているコードを見てみます。これはプルリクエストのタイトルが octocat で始まっていれば「PR title starts with 'octocat'」を出力して成功し、そうでなければ「PR title did not start with 'octocat'」を出力して失敗するというものです。
- name: Check PR title run: | title="${{ github.event.pull_request.title }}" if [[ $title =~ ^octocat ]]; then echo "PR title starts with 'octocat'" exit 0 else echo "PR title did not start with 'octocat'" exit 1 fi
引用元でも示されている通り、問題は title="${{ github.event.pull_request.title }}"
の部分にあります。もしプルリクエストのタイトルが a"; ls $GITHUB_WORKSPACE"
のようになっていると以下のように展開されて ls
コマンドが実行されてしまいます。
title="a"; ls $GITHUB_WORKSPACE""
これを応用すれば機密情報を外部に送信するといった悪質なコードも実行可能なことは想像に難くないと思います。業務で書かれるワークフローであればプルリクエストを作成可能な人は限られるかもしれませんが、攻撃の意図がなくともプルリクエストのタイトルに "
を含めてしまいワークフローがエラーを起こしてしまうといったことは十分にありえます。
脆弱性が生じる背景
このような脆弱性が生じる背景にあるのがシェルスクリプトの生成です。ポイントは run:
部分がそのままシェルスクリプトとして実行されるのではなく、 run:
部分の ${{ ... }}
を置換する形で生成されたシェルスクリプトが実行されるところです。生成の過程でシェルスクリプトの文法が全く考慮されていないために問題が生じます。
文法の考慮がないということは、 "${{ github.event.pull_request.title }}"
の両端にある "
の対応関係が維持される保証がないということです。先述の例ではプルリクエストのタイトルに含まれる "
によって対応関係を崩されたことが ls
の実行に繋がっています。
脆弱性を防ぐ方法
こうした脆弱性を防ぐコツは極力 run:
部分で ${{ ... }}
を使わないことです。 ${{ github.run_number }}
のように形式が決まっており脆弱性が生じないように使えるものもありますが、コードレビューで一つ一つ確認するのも手間なので使わない方に倒すのが楽ではないかと思います。
環境変数を使うと run:
部分から ${{ ... }}
をなくせます。先の例であれば下記の要領です。env:
部分の ${{ ... }}
には文法上の問題がないのでインジェクションはできません。
- name: Check PR title env: TITLE: ${{ github.event.pull_request.title }} run: | if [[ "$TITLE" =~ ^octocat ]]; then echo "PR title starts with 'octocat'" exit 0 else echo "PR title did not start with 'octocat'" exit 1 fi
環境変数は必ずしも自前で用意しなければならないわけではなく、デフォルトで設定される環境変数を利用可能な場合も多いです。
ツールを使うのも有効な手段です。例えば actionlint には Script injection by potentially untrusted inputs をチェックしてくれる機能があります。
類似の問題
類似の問題は GitHub Actions に限らず色々な場所に現れるので注意が必要です。printfアンチパターンと呼ぶ方もいるようです。
ここでは GitHub Actions のワークフローからもうひとつ例を考えてみます。以下はワークフローを動かしているブランチ宛のプルリクエスト一覧を出すコードです。
- run: | gh pr list \ --jq '.[] | select(.baseRefName=="${{ github.head_ref }}") | .number' \ --json baseRefName,number
- run: | gh pr list \ --jq ".[] | select(.baseRefName==\"$GITHUB_HEAD_REF\") | .number" \ --json baseRefName,number
- run: | gh pr list \ --jq '.[] | select(.baseRefName==env.GITHUB_HEAD_REF) | .number' \ --json baseRefName,number
1番目の書き方は既に見たように問題があります。GitHub のドキュメント(Understanding the risk of script injections)でも言及されている通り zzz";echo${IFS}"hello";#
のような文字列もブランチ名として正当だからです。
2番目の書き方にも問題があります。これには ${{ ... }}
が含まれていないのでシェルスクリプトの生成では問題が生じません。しかしシェルスクリプトが jq のフィルタを生成する過程で問題が生じます。シェルスクリプトが $GITHUB_HEAD_REF
を展開する際に jq の文法は考慮されないため \"$GITHUB_HEAD_REF\"
の両端にある \"
の対応関係を崩せてしまいます。
3番目の書き方であれば問題ありません。どの過程においても jq のフィルタ .[] | select(.baseRefName==env.GITHUB_HEAD_REF) | .number
そのものには変化を加えられないからです。このように外側のシェルスクリプト等の変数展開に頼らないことが安全に書くコツと言えるかもしれません。
おわりに
今回は GitHub Actions のワークフローの安全な書き方についてご紹介しました。思わぬ事故を減らす手助けになれば嬉しく思います。
エモーションテックでは顧客体験・従業員体験の改善をサポートし世の中の体験を変えるプロダクトを、GitHub Actions も活用しながら開発しております。もし興味を持っていただけましたら採用ページからご応募をお願いいたします。