OVHcloud CLIのリリース差し替えから学ぶ、GitHub Releasesの改ざん対策:イミュータブルリリースとgh release verify-asset

OVHcloud CLIのリリース差し替えから学ぶ、GitHub Releasesの改ざん対策:イミュータブルリリースとgh release verify-asset

OVHcloud CLIの既存リリース5件のバイナリが、乗っ取られた開発者アカウントから差し替えられ、少なくとも3件ではchecksums.txtも差し替えられました。公開ハッシュの照合で気付けない理由と、イミュータブルリリースとリリース証明による守り方を整理します。
2026.10.26

どうも!オペ部の西村祐二です!

2026年10月19日、OVHcloud CLI の GitHub Releases で、公開済みの v0.11.0〜v0.15.0 のバイナリが、秘密情報を盗むモジュールを埋め込んだものに差し替えられました。ソースコードもタグも CI も変わっていません。一部のリリースでは checksums.txt まで差し替えられていたため、「公開されているハッシュと一致するか」を確かめても気付けない状態でした。この記事では、OVHcloud のアドバイザリと、発見のきっかけになった Issue #300 をもとに、GitHub Releases でバイナリを配布する側と取得する側が、それぞれ確認しておきたい設定と検証方法を整理します。

先に結論:確認と対策

確認すること 対策
OVHcloud CLI の v0.11.0〜v0.15.0 を、10月19日 12:47〜14:21(UTC)にダウンロードまたは更新して実行したか 端末の秘密情報を漏れたものとして失効・再発行し、バイナリを削除して v0.16.0 以降を入れ直す
自分が配布しているリポジトリで、イミュータブルリリースが有効か リポジトリか組織の設定で有効にする。有効化より前のリリースは守られないので、案内するバージョンを有効化後のリリースにする
CI やインストールスクリプトで取得するバイナリのハッシュを、リリースとは別の場所に固定しているか 初回に取得したハッシュを、自分のリポジトリの CI 定義などに書いて固定する。配布元がイミュータブルリリースなら、GitHub CLI の gh release verify-asset で照合する

何が起きたのか

アドバイザリと Issue #300 によると、流れは次のとおりです。時刻は UTC に直しています(アドバイザリは UTC+2 で記載)。

日時(UTC) 出来事 出典
2025-10-20 開発者の私物の MacBook に、拡張機能「color-picker-universal 2.8.91」が入る アドバイザリ(VS Code の拡張であることは StepSecurity)
2026-03 同じアカウントの個人リポジトリ複数が侵害される アドバイザリ
2026-10-19 12:47 v0.15.0 の Darwin 向けバイナリが差し替えられる アドバイザリ
2026-10-19 12:57 v0.15.0 のバイナリがすべて差し替えられる アドバイザリ
2026-10-19 13:30 v0.11.0〜v0.15.0 のバイナリの差し替えが終わる アドバイザリ
2026-10-19 13:50 利用者が GitHub の Issue で報告する アドバイザリ
2026-10-19 14:21 差し替えられたバイナリが削除される アドバイザリ
2026-10-19 17:16 署名付きタグの v0.16.0 がイミュータブルリリースとして公開される 署名付きタグはアドバイザリ、イミュータブルは Issue #300
2026-10-20 アドバイザリ GHSA-862f-55f5-vmvw が公開される アドバイザリ

差し替えが終わった時刻は出典によって異なります。アドバイザリは v0.11.0〜v0.15.0 の差し替えを 13:30(UTC)までとしています。Issue #300 は v0.13.0〜v0.15.0 の差し替えを 12:47〜13:12(UTC)ごろと記録し、StepSecurity の分析は v0.11.0〜v0.12.0 を含めて 13:12(UTC)に終わったとしています。

  • 規模: アドバイザリによると、悪性のバイナリが公開されていたのは約1時間半(アドバイザリの表記は「1h33」)、ダウンロードは約250件です。
  • 原因: アドバイザリは冒頭で、主な問題をリポジトリで「release immutability」が設定されていなかったこととしています。「Root cause」の節では、侵害された開発者の GitHub アカウントがリリースのバイナリの差し替えに使われた経緯を説明しています。差し替えに使われたのは開発者の私物の MacBook ではなく、盗まれた鍵(原文は「stolen keys」)だとしています。会社の端末の侵害は確認されていません。
  • 変わっていないもの: アドバイザリによると、ソースコードとビルドチェーンは影響を受けていません。Issue #300 の報告者は、v0.15.0 のタグが元のコミットを指したままであることを確かめました。Go のモジュールプロキシ(go install などが取得に使う配布元)も、そのコミットを配っていました。
  • 影響を受けた経路: StepSecurity の分析は、Homebrew、go install、ソースからのビルド、Docker イメージは影響を受けていないとしています。
  • 対象外のバージョン: v0.10.0 以前と v0.16.0 以降です。差し替えの時間帯より前に v0.11.0〜v0.15.0 をダウンロードしていた場合も対象外です(アドバイザリ)。

どうやって気付けたのか

Issue #300 の報告者は、CI で ovhcloud-cli_Linux_arm64.tar.gz の SHA-256 を固定していました。差し替え後に CI がハッシュの不一致で失敗したことで、公開から数週間たったリリースのアセットが書き換えられていると気付きました(Issue #300 の記載)。

報告者はさらに、差し替え後のバイナリに go version -m を実行し、埋め込まれたビルド情報から次の点を示しました。

  • バージョンが v0.15.1-0.20261013083505-5ead8ef9b43d+dirty で、リリースより後の 10月13日のコミットを指している
  • vcs.modified=true(未コミットの変更がある状態でビルドされている)
  • .goreleaser.yaml にある、ビルド時にバージョン文字列を埋め込む ldflag(リンカーフラグ)が入っていない

報告者はここから、リリースのパイプラインでビルドされたものではなく、手元でビルドされたものだと判断しました。

攻撃の流れ

アドバイザリと Issue #300 の内容をもとに、流れを図にしました。図は自作した概念図で、公式の構成図ではありません。

自作した、リリースアセット差し替えの流れの概念図。鍵が盗まれた開発者の GitHub アカウントが、既存の v0.11.0〜v0.15.0 のバイナリと、v0.13.0〜v0.15.0 の checksums.txt を、削除して同じ名前で再登録する。同じリリースの checksums.txt と照合する利用者は一致してしまい実行するが、以前の SHA-256 を CI の定義に固定していた CI は不一致で失敗する。ソースコード、タグ、CI のワークフローは変わっていない。

利用者は差し替え後のバイナリと、それに合わせた checksums.txt を同じ URL から受け取るため、照合が通ってしまいます。リリースとは別の場所にハッシュを固定していた CI だけが、不一致で失敗しました。

  1. 前提条件: 攻撃者が、リポジトリのリリースを変更できるアカウントの認証情報を持っている。対象のリリースがイミュータブルリリースになっていない。
  2. 侵入・悪用: 新しいリリースもタグも作らず、既存のリリースのアセットだけを差し替える。v0.13.0〜v0.15.0 では、同じリリースの checksums.txt も差し替え、公開されているハッシュを差し替え後のファイルに合わせている(Issue #300、StepSecurity)。
  3. 影響: 同じ URL からダウンロードして実行した端末で、本来の CLI の機能に加えて秘密情報の収集が動く。アドバイザリによると、対象は次のとおり。
    • エディタと AI アシスタントのデータベースや設定(VS Code、Cursor、Continue、Sourcegraph Cody、OpenCode、Hermes、Claude など)
    • AWS / Google Cloud / Azure / DigitalOcean の認証情報
    • Docker / Kubernetes の設定、SSH 鍵、npm、Terraform、.netrc、git のグローバル設定

なぜ成立するのか

公開済みのアセットは、書き込み権限があれば変えられる

GitHub Releases のアセットは、イミュータブルリリースでなければ、公開後も削除や再アップロードができます。REST API のドキュメントは、アセットの編集について「Users with push access to the repository can edit a release asset.」と書いています。編集で変えられるのは名前やラベルなどで、ファイルの中身を変えるには、古いファイルを削除してから同じ名前で再アップロードします。リリースの URL は変わらないので、利用者には、いつもと同じ場所から同じ名前のファイルを取得しているように見えます。

同じリリースの checksums.txt は、同じ権限で書き換えられる

checksums.txt を同じリリースのアセットとして置いている場合、バイナリを差し替える権限を持つ人は、checksums.txt も差し替えられます。同じ場所から取得したハッシュとの照合は、転送中に壊れていないかの確認にはなりますが、公開者以外が差し替えていないかの確認にはなりません。今回気付けたのは、報告者が以前に取得したハッシュを、リリースとは別の場所(自分の CI)に固定していたからです。

ソース、タグ、CI の履歴には現れない

差し替えはアセットの削除と再アップロードだけで済むため、コミット履歴や Actions の実行履歴には残りません。リポジトリの履歴を監視していても気付けません。

イミュータブルリリースは「これから公開するリリース」だけを守る

イミュータブルリリースを有効にすると、公開したリリースのアセットは変更も削除もできなくなり、タグも動かせなくなります。ただし設定のドキュメントは「Be aware that immutability will only apply to future releases.」と書いています。有効にする前に公開したリリースは、有効化後も差し替えられる状態のまま残ります。

自分の環境が影響を受けるか確かめる

OVHcloud CLI を使っている場合

  • 10月19日 12:47〜14:21(UTC)に、v0.11.0〜v0.15.0 をダウンロードまたは更新して実行したかを確認します。
  • 展開した ovhcloud(Windows は ovhcloud.exe)本体を実行せずに MD5 を計算し(macOS は md5 ovhcloud、Linux は md5sum ovhcloud)、アドバイザリ付録の値(次の表)と照合します。表の値は本体の MD5 で、ダウンロードした tar.gz や zip の値ではありません。
対象 MD5(アドバイザリ記載)
Darwin arm64 6f2f086239f3d88eb2750e871901927d
Darwin x86_64 347e65eb7f2d78d1c6872690272a6cc3
Linux arm64 01f57206d37c28cf711677f612052913
Linux x86_64 301bff0b51980b9e76c93633e7cfd3bd
Windows x86_64(ovhcloud.exe) 6a3cf153338c416169cda2fe704c8848
  • 一時ディレクトリに envchecker- で始まるフォルダが残っていないかを確認します。アドバイザリによると、このフォルダは実行後も削除されず、影響を受けた手がかりになります。
  • アドバイザリは送信先の URL として hxxps://45.32.148[.]174/upload を挙げ(IOC の一覧では 45.32.148[.]174:443)、この URL は攻撃者がいつでも変えうるとしています。

アドバイザリは、該当した場合の対応として、その端末の秘密情報を漏れたものとして扱い、失効または無効化すること、バイナリを削除すること、v0.16.0 以降を入れ直すことを挙げています。

GitHub Releases でバイナリを配布している場合

  • リポジトリの Settings の「Releases」で「Enable release immutability」が有効になっているかを確認します。組織で管理している場合は、組織の Settings >「Repository」>「General」の「Releases」も見ておきます。ポリシーが「All repositories」または「Selected repositories」になっていれば、対象のリポジトリで有効になります(設定のドキュメント)。
  • 利用者に案内しているバージョンがイミュータブルかは、gh release verify で確かめられます。以下のコマンドは、gh auth login で認証済みであることを前提とします。
gh release verify v1.2.3 --repo OWNER/REPO
  • Web の Releases 画面では、イミュータブルなリリースに「Immutable」のラベルが付きます(検証のドキュメント)。

  • 既存リリースのアセットが公開後に変わっていないかは、REST API が返すアセットの created_at、updated_at、uploader、digest から見当を付けられます。各フィールドは REST API のドキュメントにある応答の項目です。下のコマンドは、タグ名とリリースの公開日時(published_at)の横に、アセットごとの値を並べます。

gh api "repos/OWNER/REPO/releases?per_page=100" --paginate \
  --jq '.[] | .published_at as $p | .tag_name as $t | .assets[] | [$t, $p, .name, .created_at, .updated_at, .uploader.login, .digest] | @tsv'

以下の見方は、ドキュメントのフィールドをもとに自分なりに整理したものです。

  • ファイルの中身を変えるには削除と再アップロードが必要なので、差し替えられたアセットは created_at が新しくなります。リリースの公開から大きく時間が空いて作られたアセットがあれば、公開後に追加・差し替えされた可能性があります。ただし、リリースを公開してからアセットを順に上げるツールでは、通常の流れでも数秒〜数分の差は出ます。
  • uploader は、普段リリースを作らないアカウントを見つける手がかりになります。今回も、差し替えられたアセットの uploader は元のアセットを上げたアカウントと違っていました(Issue #300)。ただし、普段リリースを上げるアカウント自体が乗っ取られた場合は、uploader だけでは見分けられません。
  • digest は、1回取得しただけでは変化が分かりません。以前に記録したハッシュ(CI に固定した値など)と比べて初めて差し替えに気付けます。古いアセットでは null(出力は空欄)になることがあります。

CI やインストールスクリプトで GitHub Releases からバイナリを取得している場合

  • ハッシュを照合しているか、しているなら照合用のハッシュをどこから取っているかを確認します。同じリリースの checksums.txt から毎回取っているだけなら、前述のとおり差し替え後のファイルと一致してしまいます。
  • 配布元がイミュータブルリリースを使っているなら、gh release verify-asset で手元のファイルを確かめられます。このコマンドは、ファイルのダイジェストがリリース証明の対象と一致するか、その証明が指定したリリースのものかを検証します(GitHub CLI のマニュアル)。リリース証明は、タグ、コミット SHA、アセットを含む署名付きの記録です(イミュータブルリリースのドキュメント)。
gh release verify-asset v1.2.3 ./my-tool_Linux_x86_64.tar.gz --repo OWNER/REPO

検証のドキュメントによると、リリースの「Source code」の zip と tarball はダウンロード時に生成されるため、このコマンドでは確かめられません。

守り方

配布する側

  • イミュータブルリリースを有効にします(確認する設定の場所は前述)。
  • 同じドキュメントは、リリースを下書きで作り、アセットをすべて付けてから公開する流れを推奨しています。公開した時点でアセットが固定されるためです。
  • イミュータブルリリースを公開するとリリース証明が自動で作られ、利用者は gh release verify-asset で照合できるようになります。
  • 利用者に案内するバージョンを、有効化した後のリリースに切り替えます。OVHcloud は組織全体でイミュータブルリリースを有効にし(アドバイザリ)、v0.16.0 をイミュータブルリリースとして公開して最新版にしました(Issue #300)。

取得する側

  • 初回に取得したハッシュを、CI やパッケージマネージャーの定義など、リリースとは別の場所に固定します。
  • 配布元がイミュータブルリリースを使っているなら、前述の gh release verify-asset を取得の手順に組み込みます。
  • 今回影響を受けなかった経路もあります。前述のとおり Homebrew 経由のインストールは無事で、StepSecurity によると Homebrew の cask は SHA-256 を固定していました。手動でダウンロードする以外の経路があれば、そちらを使う選択肢もあります。

開発者のアカウントと端末

アドバイザリは、開発者の私物端末に入った拡張機能による侵害を経緯の起点として説明しています。リリースの差し替えに使われた鍵がその端末から盗まれたとは、直接は書かれていません。それでも、次の2点は棚卸ししておきます。

  • リポジトリに書き込めるアカウントの鍵やトークンが、どの端末に残っているか
  • その端末に、どの拡張機能が入っているか

検知の手がかり

  • ハッシュを固定した CI のジョブが、変えていないはずの依存で失敗し始めたら、まず配布元のリリースアセットが差し替えられていないかを疑います。
  • 既存のリリースは、前述の REST API の確認方法で点検できます。
  • Go のバイナリなら、go version -m でビルド情報を確かめ、リリースのタグやビルド設定と食い違いがないかを見ます(今回の手がかりは前述の3点)。
  • 組織の監査ログには、イミュータブルリリース設定の有効化・無効化のイベント(repo.immutable_releases_settings_enabled / repo.immutable_releases_settings_disabled)があります。組織のポリシーを変えたときのイベント(org.update_immutable_releases_settings_policy)もあります。設定が無効にされていないかの監視に使えます。一方、イベント一覧には、アセットの追加・削除・差し替えのイベントは見当たりませんでした。

まとめ

  • OVHcloud CLI では、公開済みのリリース5件のバイナリが、乗っ取られた開発者アカウントから差し替えられ、少なくとも3件では checksums.txt も差し替えられました。ソース、タグ、CI は変わっておらず、気付けたきっかけは CI でのハッシュ固定でした。
  • 同じリリースに置いたハッシュとの照合では、差し替えに気付けません。配布する側はイミュータブルリリースを有効にし、取得する側はハッシュをリリースとは別の場所(自分のリポジトリの CI 定義など)に固定するか、gh release verify-asset でリリース証明と照合します。
  • イミュータブルリリースが守るのは有効化後に公開したリリースだけです。有効にしたら、利用者に案内するバージョンも有効化後のリリースに切り替えておく必要があります。

誰かの参考になれば幸いです。


参考リンク:

この記事をシェアする

関連記事