GitHub Actionsの良いところといえば、GitHub Actions Marketplaceにあるアクションを利用できることです。また、ワークフローもGitHubを参照して再利用できます。 では、YAMLで定義したリモートアクションやワークフローはいつ、どこにダウンロードされているのでしょうか?以前同じような内容の記事を書いたのですが、ちょっと手抜きだったのでもう少し細かく見ます。
リモートアクションがダウンロードされる流れ
リモートアクション1のダウンロードと保持されるパスはGitHub Actions ドキュメントには記載されていません。このため、将来的な挙動は変わる可能性があります。
リモートアクションは次の流れで取り扱われています。少なくともこの挙動は2023年時点から2025年3月現時点まで変わっていないようです。
- GitHub Actions YAMLで、リモートアクションを指定する
jobs: build: runs-on: ubuntu-latest steps: # 例: リモートアクションを指定する - uses: actions/checkout@v4
- ジョブ実行時に自動実行される
Set up job
でリモートアクションをダウンロード
Prepare workflow directory Prepare all required actions Getting action download info Download action repository 'actions/checkout@v4' (SHA:11bd71901bbe5b1630ceea73d27597364c9af683) Complete job name: action
- ダウンロードされたリモートアクションは、
/home/runner/work/_actions/{OWNER}/{REPO}/{REF}
に配置される
# actions/checkout@v4 なら次のパス /home/runner/work/_actions/actions/checkout/v4
リモートアクションのダウンロードパスを確認する
通常リモートアクションを利用するときは、リモートアクションがaction.yml
で提供するAPIしか触りません。実際それが目的なのでそれでいいです。例えば、actions/checkoutであれば、次のようにwith
セクションで指定します。
jobs: build: runs-on: ubuntu-latest steps: # 例: リモートアクションの提供するAPIを利用する - uses: actions/checkout@v4 with: fetch-depth: 0 sparse-checkout: | .github
今回示すダウンロードされたリモートアクションのフォルダはどのようにダウンロードされるか見てみましょう。例えば次のようなワークフローを作成します。
name: remote actions download path on: workflow_dispatch: jobs: action: runs-on: ubuntu-24.04 timeout-minutes: 3 steps: - uses: actions/checkout@v4 - name: Downloaded actions from the marketplace run: ls -l /home/runner/work/_actions - name: See actions download path run: ls -l /home/runner/work/_actions/actions/checkout/ - name: See actions download contents run: ls -lR /home/runner/work/_actions/actions/checkout/v4 - name: Cat action's src/main.ts run: cat /home/runner/work/_actions/actions/checkout/v4/src/main.ts
実行ログを順に見てみましょう。
まずはDownloaded actions from the marketplace
です。これを見ると、ジョブで利用するリモートアクションが/home/runner/work/_actions
にダウンロードされていることがわかります。複数のリモートアクションを1ジョブで利用していれば、それぞれがダウンロードされていることがわかります。
$ ls -l /home/runner/work/_actions total 4 drwxr-xr-x 3 runner docker 4096 Mar 4 18:36 actions
次はSee actions download path
で、リモートアクションのREFが何になるのかを確認します。パスを見るとactions/checkout@REF
のREFで指定したv4というフォルダが作成されていることがわかります。REFにはブランチも指定できるので、foo/bar
というブランチを指定した場合は、/home/runner/work/_actions/actions/checkout/foo/bar
というフォルダが作成されます。
$ ls -l /home/runner/work/_actions/actions/checkout/ total 8 drwxr-xr-x 9 runner docker 4096 Mar 4 18:36 v4 -rw-r--r-- 1 runner docker 19 Mar 4 18:36 v4.completed
実際にダウンロードされたフォルダの中身をSee actions download contents
でみると、このREFのGitHubリポジトリの内容がそのままダウンロードされていることがわかります。この例では、actions/checkout@v4
のv4
ブランチの内容がダウンロードされています。これが私には衝撃でした。リモートアクションはリモートアクションのAPIでしか触れないようにマジックでも使ってるのかと思っていましたが、普通にダウンロードされています。2
$ ls -lR /home/runner/work/_actions/actions/checkout/v4 /home/runner/work/_actions/actions/checkout/v4: total 332 -rw-r--r-- 1 runner docker 8062 Oct 23 14:24 CHANGELOG.md -rw-r--r-- 1 runner docker 26 Oct 23 14:24 CODEOWNERS -rw-r--r-- 1 runner docker 1297 Oct 23 14:24 CONTRIBUTING.md -rw-r--r-- 1 runner docker 1097 Oct 23 14:24 LICENSE -rw-r--r-- 1 runner docker 9359 Oct 23 14:24 README.md drwxr-xr-x 2 runner docker 4096 Mar 4 18:36 __test__ -rw-r--r-- 1 runner docker 4593 Oct 23 14:24 action.yml drwxr-xr-x 2 runner docker 4096 Mar 4 18:36 adrs drwxr-xr-x 2 runner docker 4096 Mar 4 18:36 dist drwxr-xr-x 2 runner docker 4096 Mar 4 18:36 images -rw-r--r-- 1 runner docker 253 Oct 23 14:24 jest.config.js -rw-r--r-- 1 runner docker 264924 Oct 23 14:24 package-lock.json -rw-r--r-- 1 runner docker 1500 Oct 23 14:24 package.json drwxr-xr-x 3 runner docker 4096 Mar 4 18:36 src -rw-r--r-- 1 runner docker 334 Oct 23 14:24 tsconfig.json /home/runner/work/_actions/actions/checkout/v4/__test__: total 144 -rw-r--r-- 1 runner docker 28440 Oct 23 14:24 git-auth-helper.test.ts -rw-r--r-- 1 runner docker 9474 Oct 23 14:24 git-command-manager.test.ts -rw-r--r-- 1 runner docker 14839 Oct 23 14:24 git-directory-helper.test.ts -rw-r--r-- 1 runner docker 4870 Oct 23 14:24 git-version.test.ts -rw-r--r-- 1 runner docker 5032 Oct 23 14:24 input-helper.test.ts -rwxr-xr-x 1 runner docker 210 Oct 23 14:24 modify-work-tree.sh -rwxr-xr-x 1 runner docker 134 Oct 23 14:24 override-git-version.cmd -rwxr-xr-x 1 runner docker 180 Oct 23 14:24 override-git-version.sh -rw-r--r-- 1 runner docker 5982 Oct 23 14:24 ref-helper.test.ts -rw-r--r-- 1 runner docker 2385 Oct 23 14:24 retry-helper.test.ts -rw-r--r-- 1 runner docker 3450 Oct 23 14:24 url-helper.test.ts -rwxr-xr-x 1 runner docker 1195 Oct 23 14:24 verify-basic.sh -rwxr-xr-x 1 runner docker 353 Oct 23 14:24 verify-clean.sh -rwxr-xr-x 1 runner docker 445 Oct 23 14:24 verify-fetch-filter.sh -rwxr-xr-x 1 runner docker 216 Oct 23 14:24 verify-lfs.sh -rwxr-xr-x 1 runner docker 596 Oct 23 14:24 verify-no-unstaged-changes.sh -rwxr-xr-x 1 runner docker 258 Oct 23 14:24 verify-side-by-side.sh -rwxr-xr-x 1 runner docker 1217 Oct 23 14:24 verify-sparse-checkout-non-cone-mode.sh -rwxr-xr-x 1 runner docker 1352 Oct 23 14:24 verify-sparse-checkout.sh -rwxr-xr-x 1 runner docker 263 Oct 23 14:24 verify-submodules-false.sh -rwxr-xr-x 1 runner docker 739 Oct 23 14:24 verify-submodules-recursive.sh -rwxr-xr-x 1 runner docker 692 Oct 23 14:24 verify-submodules-true.sh /home/runner/work/_actions/actions/checkout/v4/adrs: # 省略...
最後に、Cat action's src/main.ts
で、リモートアクションの中身を確認します。この例では、actions/checkout@v4
のv4
ブランチのsrc/main.ts
を確誋します。
$ cat /home/runner/work/_actions/actions/checkout/v4/src/main.ts import * as core from '@actions/core' import * as coreCommand from '@actions/core/lib/command' import * as gitSourceProvider from './git-source-provider' import * as inputHelper from './input-helper' import * as path from 'path' import * as stateHelper from './state-helper' async function run(): Promise<void> { try { const sourceSettings = await inputHelper.getInputs() try { // Register problem matcher coreCommand.issueCommand( 'add-matcher', {}, path.join(__dirname, 'problem-matcher.json') ) // Get sources await gitSourceProvider.getSource(sourceSettings) core.setOutput('ref', sourceSettings.ref) } finally { // Unregister problem matcher coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '') } } catch (error) { core.setFailed(`${(error as any)?.message ?? error}`) } } async function cleanup(): Promise<void> { try { await gitSourceProvider.cleanup(stateHelper.RepositoryPath) } catch (error) { core.warning(`${(error as any)?.message ?? error}`) } } // Main if (!stateHelper.IsPost) { run() } // Post else { cleanup() }
挙動を試す
いくつか挙動を試します。
actions/runnerコード上の定義
runnerコード上はテストコードでaction_path
として渡すときに定義されています。
// これ inputGitHubContext["action_path"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_actions/owner/composite/main"); inputGitHubContext["action"] = new StringContextData("__owner_composite"); inputGitHubContext["api_url"] = new StringContextData("https://api.github.com/custom/path"); inputGitHubContext["env"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp/_runner_file_commands/set_env_265698aa-7f38-40f5-9316-5c01a3153672"); inputGitHubContext["path"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp/_runner_file_commands/add_path_265698aa-7f38-40f5-9316-5c01a3153672"); inputGitHubContext["event_path"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp/_github_workflow/event.json"); inputGitHubContext["repository"] = new StringContextData("owner/repo-name"); inputGitHubContext["run_id"] = new StringContextData("2033211332"); inputGitHubContext["workflow"] = new StringContextData("Name of Workflow"); inputGitHubContext["workspace"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/step-order/step-order"); inputeRunnerContext["temp"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_temp"); inputeRunnerContext["tool_cache"] = new StringContextData("/home/username/Projects/work/runner/_layout/_work/_tool");
GitHub Actions Contextのaction_path
にセットされています。ただ、GitHubコンテキストや環境変数には現れないんですよね
// Set GITHUB_ACTION_PATH step.ExecutionContext.SetGitHubContext("action_path", ActionDirectory);
リモートアクションのREFは動的に指定できない
この流れの中で、いじりたくなったのがダウンロードされるリモートアクションの起点であるuses: actions/checkout@v4
の部分です。幸いにもここは静的でないと機能しないように縛りがかかっています。例えばinputs
でREFを得て、ワークフローで取得するように書いてもワークフローは不正として実行されません。これは悪意のある攻撃者による動的な攻撃がしにくいことを示すので良い点です。
inputs: ref: description: 'The ref to checkout. Default is the default branch of the repository.' required: false default: ${{ github.ref }} jobs: build: runs-on: ubuntu-latest steps: # usesは静的でないとエラーになる - uses: actions/checkout@${{ inputs.ref }}
リモートアクションのファイルに無制限で触れる
この方法は、リモートアクションのリポジトリにある内容を無制限に触れます。リモートアクションのリポジトリに何か設定ファイルなどをおいて、利用側で読ませるといったこともできます。サブモジュールでもいいじゃないかという指摘もあるしれませんが、サブモジュールは維持・更新重いんですよね。リモートアクションv1のこのファイルはあることが期待できるので読みたいだけ、という時にcurlなどよりも簡単に利用できるので便利です。
無制限にアクセスできるので懸念もあります。例えば、有名なリポジトリに誤認させるオーナー名、リポジトリを用意すれば利用を誤認させられます。action.ymlで直接悪さする処理を書いたり、このファイルパスを利用してさらに仕込みを入れるケースもあり得るでしょう。こういった挙動は十分懸念されるべきです。
従来からいわれている通り3rdパーティのリモートアクションはハッシュタグで指定する安全対策は、シンプルですがハッシュ衝突がない限りは有効な手法と考えられます。