Composer CVE-2026-45793でGitHub ActionsトークンがCIログに平文露出
目次
TL;DR
影響 Composer 2.3.0以上2.9.8未満、2.0.0以上2.2.28未満、1.0以上1.10.28未満。
GitHub Actionsの GITHUB_TOKEN やGitHub App installation tokenがComposerの auth.json に入るワークフロー
対応 Composer 2.9.8、2.2.28 LTS、または1.10.28へ更新。Composerを固定しているCIイメージ、shivammathur/setup-php の tools: composer:<version> 指定、古いビルドコンテナを確認
ログ 2026年5月13日UTC前後の失敗したComposer実行ログを監査。該当があればログ削除、トークン失効、予期しないGitHub操作の確認
期限 GitHubは新形式トークンのロールアウトを一度戻し、2026年5月18日14:00 UTC以降に再開予定。日本時間では2026年5月18日23:00以降
Composerの脆弱性CVE-2026-45793は、コード実行ではなくログへの認証情報露出だ。
ただ、出る場所がGitHub Actionsのジョブログなので、公開リポジトリや長めに動くCIではそのままリポジトリ操作権限になる。
PackagistのNils Adermannは2026年5月13日に、Composer 2.9.8と2.2.28 LTSへの更新を呼びかけた。
GitHubがActions向けトークンの新形式を段階導入し、その文字列にハイフンが入るようになった。
Composer側の検証正規表現は2021年のGitHubトークン形式を前提にしていて、ハイフンを許可していなかった。
失敗時の挙動が悪かった。
Composerは「GitHub OAuthトークンに無効な文字がある」という例外を投げるとき、拒否したトークン文字列をそのまま例外メッセージへ埋め込んでいた。
Symfony Consoleがその例外をstderrへ出し、CIがstderrをジョブログとして保存する。
GitHub Actionsのシークレットマスキングは完全な連続文字列を伏せる設計なので、例外表示の折り返しやANSI制御文字の混入で伏字化に失敗する場合がある。
Composerが弾いたのは正しいトークンだった
問題の検証は Composer\IO\BaseIO::loadConfiguration() にあった。
GHSAの説明では、古い正規表現は次の文字だけを許可していた。
^[.A-Za-z0-9_]+$
GitHubの新しいActionsトークンは ghs_<numeric-id>_<base64url-JWT> のような形になる。
JWT部分はbase64urlでエンコードされるため、URL安全な文字として - と _ を使う。
_ は通るが、- は通らない。
Composerから見ると、形式の変わった正規トークンが「不正な文字を含むトークン」に見える。
その結果、ビルドを止めるためのエラー出力が、そのまま認証情報の漏洩経路になった。
修正版では2つの変更が入った。
例外メッセージから拒否トークンの値を削除し、検証側もハイフンを受け入れる。
新しい正規表現は次のようにハイフンを許可する1文字だけの差だ。
^[.A-Za-z0-9_-]+$
Composer公式の2.9.8変更履歴にも、GitHub token validation and disclosureの修正としてGHSA-f9f8-rm49-7jv2が載っている。
たった1文字の追加漏れが、エラーメッセージ経由で平文トークンをCIログへ流す経路になった。
リーク経路をまとめると次のチェーンになる。
flowchart TD
A["GitHubが新形式GITHUB_TOKENを段階導入"] --> B["トークン文字列にハイフンが含まれる"]
B --> C["Composerの旧正規表現が<br/>不正トークンとして拒否"]
C --> D["例外メッセージに<br/>トークン文字列を埋め込み"]
D --> E["Symfony Consoleがstderrへ出力"]
E --> F["GitHub Actionsがstderrを<br/>ジョブログとして保存"]
F --> G["マスキングが折り返し・<br/>ANSI制御文字で失敗"]
G --> H["ジョブログ上で<br/>平文トークン可視"]
GitHub Actionsのシークレットマスキングはなぜ抜けるのか
GitHub Actionsは、ワークフローのシークレットや GITHUB_TOKEN の値をジョブログ上で *** に置き換える仕組みを持つ。
ただしこのマスキングは、ログ行のテキスト中に「シークレット文字列が完全一致で連続して現れる」場合だけ動くリテラル文字列検索だ。
処理単位は1行ごとで、複数行にまたがった結合は行われない。
このため、連続性が崩れる以下のケースでマスクが効かなくなる。
- 行の折り返し: ターミナル幅や
wordwrap()で長い例外メッセージが途中で改行されると、トークン文字列の途中に改行が入って完全一致しなくなる - ANSI制御文字の挿入: Symfony Consoleはエラー出力に色やスタイルを付けることがあり、トークン文字列の内側に
\e[31mのようなエスケープシーケンスが入ると一致が外れる - エンコード境界での部分露出: トークンの一部だけが別フォーマットで切り出されて出力されると、文字列としてはトークンに見えても登録されている完全な値とずれる
- URLやdiff表示への埋込: 一部のスタックトレースはトークンを記号で囲んで出すことがあり、囲み記号でリテラル検索の条件が崩れる
「トークンはマスクされる」というのは正確には「文字列が無加工のままログ行内に連続して現れるならマスクされる」ということだ。
今回のComposerのケースでは、例外メッセージへの色付け経路と長文の折り返し経路の両方が絡んでリテラル検索が外れた。
GitHubは公式ドキュメントでもこの設計を明示しているが、ワークフロー側からは「マスクが効く前提」で読まれやすい。
シークレットがログに現れる経路自体を消す対策(例外メッセージのサニタイズ、stderrのファイル退避、トークンを処理する箇所でのtry/catch)が、マスキングと別のレイヤーで必要になる。
setup-phpを使うだけで踏むワークフローがある
手作業で auth.json にGitHubトークンを入れている環境だけの話ではない。
GHSAでは、広く使われているActionの例として shivammathur/setup-php が挙げられている。
このActionはワークフローの GITHUB_TOKEN をComposerのグローバル auth.json に自動登録することがある。
つまり、PHPプロジェクトでよくある次の流れだけで条件がそろう。
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- run: composer install
setup-php 側は修正済みComposerを拾うよう対応済みだが、ワークフローやビルドイメージでComposerのバージョンを固定していると古いまま残る。
composer:v2 のような追従指定なら拾えるが、composer:2.9.7 や独自コンテナ内の固定バイナリは別途入れ替える。
ここはMini Shai-HuludがTanStack・Mistralへ広がった件と同じく、GitHub Actions runner上のトークンが「ログ上はマスクされるはず」という前提で守れない部分だ。
Mini Shai-HuludはrunnerメモリからOIDCトークンを拾った。
今回はComposerのエラー表示がログへ平文を落とす。
どちらも、CIが持つ短命トークンを短命だから安全と読み切れない。
有効時間は短いがゼロではない
GitHub Advisoryでは、通常のGitHub-hosted runnerの GITHUB_TOKEN はジョブ終了時、遅くとも6時間で失効すると説明している。
多くの場合、Composerの例外でジョブ自体が落ちるため、その時点でトークンもすぐ失効する。
セルフホストランナーでは話が少し変わる。
GHSAとPackagistの説明では、セルフホストランナー上の GITHUB_TOKEN は発行から最大24時間有効になりうる。
GitHub Appから明示的に発行したinstallation access tokenは通常1時間だが、ワークフローの permissions: より広い権限を持つ場合がある。
Sansecは、ジョブがまだ in_progress の間に漏洩トークンを使ってGitHub APIへ到達できることを検証したとしている。
Composerエラーで即終了する単純なジョブなら窓は短い。
それでも、エラーを握りつぶして後続処理が走る構成、長いテストやデプロイを同じジョブで続ける構成、セルフホストランナーでは確認範囲を広く取る。
漏れたトークンで攻撃者ができること
GITHUB_TOKEN で何ができるかは、ワークフロー側の permissions: ブロックで決まる。
明示していない場合は、リポジトリ単位の「Workflow permissions」設定値がデフォルトとして適用される。
GitHubは2023年以降、新規リポジトリのデフォルトを「contents: read のみ」に変えているが、古いリポジトリや組織には寛容なデフォルトのまま残っているものがある。
そこにトークンが漏れた場合に攻撃面として考えられるもの。
- contents: write: ブランチへの直接push、tag作成、release作成。改ざんしたコミットをmainブランチに混ぜられる
- packages: write: GitHub Packagesへのパッケージ公開・上書き。利用者のCIへ改ざんパッケージが流れる
- actions: write: workflowファイルの書き換え、実行中ジョブのキャンセル
- id-token: write: OIDCトークンを他クラウドへ提示し、AWS/GCP/Azureの一時クレデンシャルへ昇格(クラウド側のtrust policyに依存)
- issues / pull-requests: write: PRコメント差し替え、レビューステータスの偽装
GitHub Appのinstallation access tokenはさらに範囲が広い。
通常1時間有効で、App側に付与されたスコープに応じて複数リポジトリへ跨ぐ可能性がある。
Private PackagistやComposerリポジトリ連携で組織横断アクセスを持つAppのトークンが漏れた場合、被害範囲がリポジトリ単位を超える。
パブリックリポジトリのPRビルドではジョブログが外部からも閲覧できるので、攻撃者は能動的な侵入なしにログを監視するだけで足りる。
プライベートリポジトリでも、組織メンバーが多くなれば「全員が信頼できる」前提では守れない。
permissions: を最小化する設計に寄せる
トークンが漏れたときの被害を構造的に抑えるのが permissions: の最小化だ。
組織全体で「Workflow permissions」のデフォルトを read-only に倒し、書き込みが必要なジョブだけ明示的に持ち上げる。
# ワークフロー全体のデフォルト(最小権限)
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
# このジョブは contents: read のままで動く
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
- run: composer install
- run: composer test
release:
runs-on: ubuntu-latest
# 書き込みが必要なジョブだけ permissions を上書き
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
- run: ./scripts/release.sh
ジョブ単位で permissions: を上書きできるので、書き込みが要らないジョブは常時read-onlyで動かせる。
今回のComposerのようにツール側の不具合でトークンが漏れても、ジョブのトークンが書き込み権限を持っていなければpushもrelease作成もできない。
ログ漏洩の根本原因はツール側のバグだが、被害の上限を決めているのはワークフロー側の権限設計だ。
GitHubは一度ロールバックし、5月18日に再開予定
Packagistの記事には時系列の更新が入っている。
2026年5月13日14:30 UTC時点で、GitHubは新形式トークンのロールアウトを一時的に戻した。
これで「今すぐGitHub Actionsを止める」状態ではなくなったが、更新猶予ができただけだ。
2026年5月14日11:00 UTCの追記では、GitHubは新形式トークンのロールアウトを2026年5月18日14:00 UTC以降に再開する意向だとされている。
JSTでは2026年5月18日23:00以降。
それまでにCIイメージ、GitHub ActionsのComposer固定、セルフホストランナーのツールキャッシュを入れ替える。
影響バージョンと修正版は次の通り。
| 系列 | 影響範囲 | 修正版 |
|---|---|---|
| Composer 2.3系以降 | >= 2.3.0, < 2.9.8 | 2.9.8 |
| Composer 2.2 LTS | >= 2.0.0, < 2.2.28 | 2.2.28 |
| Composer 1.x | >= 1.0, < 1.10.28 | 1.10.28 |
Composer 1.10.28は例外的なレガシー修正で、通常は2.xへ上げるほうがいい。
Packagist側も「1.xへ留まるより2.xへ上げる」扱いで書いている。
ログ監査は失敗ジョブから始める
確認対象は、2026年5月13日UTCごろから新形式トークンのロールバックまでに走ったGitHub Actionsだ。
とくに composer install、composer update、composer audit、Private Packagist連携、GitHub上の非公開依存を読むジョブを見る。
探す文字列は、Composerの例外メッセージそのものが早い。
Your github oauth token for github.com contains invalid characters
該当ログがあれば、平文トークンがログ本文に残っていないか確認する。
残っていた場合はログを削除し、トークンの有効期限内に予期しないGitHub API呼び出し、push、tag作成、release作成、workflow変更がなかったかを見る。
セルフホストランナーなら、ジョブ終了後も最大24時間の窓として扱う。
Composerを更新するだけなら次で済む。
composer self-update
composer --version
CIではローカルの composer.phar だけではなく、Dockerイメージ、Actionsのツールキャッシュ、社内のベースイメージ、古い setup-php 固定設定まで見る。
GitHub Actions側ではデフォルトの GITHUB_TOKEN 権限をread-onlyにし、書き込みが必要なジョブだけ permissions: で明示的に足す。
GitHub Actionsの2026年セキュリティロードマップでも、サードパーティActionや推移的依存の固定が課題として出ていたが、今回の件はActions本体の短命トークンもログ境界で漏れるという別の穴だった。
組織やエンタープライズ規模で横断的に確認するときは、audit log APIから期間を絞って引く。
admin:org または read:audit_log スコープのトークンが必要だ。
# 直近の不審なアクションをフィルタ
gh api -H 'Accept: application/vnd.github+json' \
--paginate \
"/orgs/{ORG}/audit-log?phrase=created:>=2026-05-13T00:00:00Z+action:repo.push+OR+action:workflow_run.update+OR+action:repo.update_workflow_settings_for_repository&per_page=100" \
| jq '.[] | {created_at, actor, action, repo}'
確認したいアクションは大まかに以下のグループだ。
repo.push/git.push: 予期しないブランチや力技でのforce-pushworkflow_run.update: 実行中ジョブのキャンセル、再実行の改変repository.update: Public/Private切り替え、ブランチ保護ルールの変更org.update_default_repository_permission/repo.update_workflow_settings_for_repository: ワークフロー権限の引き上げpersonal_access_token.create/installation_token.request: 異常な新規トークン発行integration_installation_request.*: GitHub Appのインストール・スコープ変更
漏れたトークンがGitHub Appのinstallation tokenだった場合、有効窓(通常1時間)の中で何が起きたかを集中して確認する。
セルフホストランナーは最大24時間、ジョブ終了後もrunnerのworkディレクトリにキャッシュが残るため、ログ削除に加えてrunner側の _work/ と環境変数履歴の確認まで広げる。
CIツール側からのトークン漏れは今回が初めてではない
「CIで一瞬しか生きないはずのトークンが、ツール側の処理を経由して長期被害につながる」パターンは繰り返し起きている。
- Codecov bash uploader(2021年): Codecovのアップロードスクリプトが改変され、CIランナーの環境変数(
GITHUB_TOKEN、AWSキー、各種APIキー)が攻撃者サーバーへ送信された。数ヶ月間にわたって複数組織のシークレットが抜かれていた - tj-actions/changed-files(2025年): 改変されたサードパーティActionがワークフローのトークン含む環境変数をdumpする経路として悪用された。23,000以上のリポジトリが影響を受けた
- shai-hulud系npmワーム: postinstallフックでCI環境変数を読み取り、攻撃者支配のWebhookへ送信する手口が複数回観測された
これらは外部からの意図的な侵入で、ツールやActionに悪意あるコードを差し込んで環境変数を抜くタイプだ。
今回のComposerは違って、ツール内部の検証ロジックがGitHubのトークン形式変更に追従できず、エラー出力経由で漏らした「内部由来の漏洩」になる。
入口は別だが、漏れた後に攻撃者が読み取って悪用できる構造は同じだ。
CI上のシークレットは「短命だから安全」ではなく、「短命のうちに使い切られる前提で守る」対象になる。
ログ削除、トークン失効、permissions: 最小化、ツール側のサニタイズ、いずれも単独では守り切れないので層を重ねる。