codfish/semantic-release-action の侵害は、単発のインシデントではない。攻撃者は GitHub Actions、CI のシークレット、npm の公開権限、そして下流の利用者までを一本の攻撃経路として設計している。脆弱性を「対応したかどうか」の点検表として見るかぎり、この線は見えない。codfish を入口に、脆弱性を攻撃経路として読むとはどういうことかを解説する。
GitHub Actions を使っているので、この報せには一瞬手が止まった。
codfish/semantic-release-action が乗っ取られた、と。
すぐに自分の全リポジトリを grep した。結論は、使っていなかった。無事だった。
そこで安心して話を終えることもできる。だが、それでは今回の事件を見誤る。
codfish を使っていたかどうかは、あくまで一点の確認にすぎない。攻撃者が見ているのは、その一点ではない。GitHub Actions、CI のシークレット、npm の公開権限、正規パッケージ、そしてそれを install する下流の利用者までをつないだ一本の攻撃経路だ。
守る側が点で見る。 攻撃者は線で見る。
この差が、今回の本質だ。
2026年6月24日、攻撃者は codfish/semantic-release-action のリポジトリに不正なコミットを強制プッシュし、v2、v3、v4、v5 を含む複数のタグを悪性コミットへ付け替えた。
Git のタグは、保護していなければ付け替えられる。GitHub Actions はワークフロー実行時にそのタグを解決する。つまり、利用者側の YAML が何も変わっていなくても、タグの向きが変われば、次の実行から攻撃者のコードが動く。
しかも偽装はかなり巧妙だった。
悪性コミットは、過去の実在するコミットの作者名、日付、メッセージを流用していた。元の Dockerfile も消さずに残してある。git log を流し見した程度では、異常に気づきにくい。
本体は action.yml にあった。これは「このアクションが何をするか」を定義するファイルだ。
攻撃者は、元は Docker で1つの処理を走らせるだけだったこのアクションを、コンポジットアクションに作り替えた。コンポジットアクションとは、複数のステップを束ねて1つのアクションとして見せる形式だ。利用者からは uses: codfish/...@v3 の1行にしか見えないが、その1行の中身は、こう組まれていた。
| ステップ | 中身 |
|---|---|
| ① | クリーンなコミットに固定した正規版を呼ぶ |
| ② | oven-sh/setup-bun で Bun ランタイムを導入する |
| ③ | 難読化された巨大な index.js を bun run で実行する |
利用者がこのアクションを1回呼ぶと、この①〜③が毎回まとめて走る。「初回は正規処理だけ、次回から窃取が増える」のではない。最初から3ステップが1つに束ねられていて、毎回まとめて実行される。
①は本来のリリース処理なので普通に成功する。ログ上はリリースが成功したように見える。その裏で、③が毎回シークレットを抜いている。しかも②③には if: always() が付いていて、①が成功しても失敗しても必ず実行される。
ステップ①が成功を演じている裏で、ステップ③が抜く。
これは「壊す」攻撃ではない。 成功しているように見せたまま、裏で認証情報を抜く攻撃だ。
そして semantic-release 用の GitHub Action には、攻撃者が欲しいものが集まりやすい。GITHUB_TOKEN、npm に公開するための NPM_TOKEN、場合によってはクラウドやリリース用の権限。CI/CD は、開発者が信頼している自動化の場所であると同時に、攻撃者から見ると認証情報の集積所でもある。
ここで止まると、話は「codfish を使っていた人は確認しましょう」で終わる。
だが今回の問題は、そこではない。
codfish のペイロードには、thebeautifulsnadsoftime という文字列が含まれていた。これは、同時期に観測されている複数のサプライチェーン攻撃と共通するマーカーだ。
npm、PyPI、GitHub Actions。 Python、Bun。 パッケージ、CI/CD、開発者環境。
攻撃は一つの場所に閉じていない。エコシステムの境界をまたいでいる。Aikido は Miasma 系ツールキットとの関連を指摘し、他のベンダーは Shai-Hulud、Miasma、Hades などの呼称で近い流れを追っている。呼称や帰属は揺れているが、見るべきものは名前ではない。
見るべきなのは、攻撃者が配布経路そのものを狙っているという構造だ。
なぜ OSS 開発者が狙われるのか。
答えは単純だ。 開発者本人が最終目的ではないからだ。
攻撃者が本当に欲しいのは、その開発者の先にいる利用者だ。多くの人が依存しているパッケージのメンテナを一人乗っ取れば、毒を仕込んだ更新版を「正規の新バージョン」として配布できる。それを使っている開発者、CI環境、サービス利用者へ、信頼された経路を通じて一気に届く。
一般ユーザーを一人ずつ攻撃するより、配布ハブを一つ押さえるほうが効率がいい。
codfish が狙われた理由もここにある。semantic-release は、リリース作業を自動化する。つまり、パッケージを世の中へ出す経路に近い。そこにあるトークンを盗めば、次は正規パッケージに毒を混ぜられる。
攻撃は、こういう線として設計されている。
| 段 | 攻撃者が見ているもの |
|---|---|
| ① GitHub Action のタグを書き換える | 実行コードを差し替える入口 |
| ② CI で悪性コードを動かす | 開発者が信頼している実行環境 |
③ NPM_TOKEN などを盗む | 配布権限への鍵 |
| ④ 正規パッケージに毒入り版を公開する | 信頼された配布チャネル |
| ⑤ 利用者が install する | 下流へ自動的に広がる出口 |
守る側は、これを別々の問題として処理しがちだ。
タグはタグの問題。 シークレットはシークレットの問題。 npm は npm の問題。 CI は CI の問題。 CVE は CVE の問題。
だが攻撃者は、最初からそれらを一本の経路として見ている。
ここが重要だ。 点の集合と、攻撃経路は同じものではない。
点をいくつ潰しても、攻撃者は別の点をつないで線を作る。だから「codfish は使っていなかった」「この CVE は対応済みだった」だけでは、面で来る攻撃には足りない。
CVE は重要だ。既知の脆弱性を識別し、対応状況を管理するには欠かせない。
だが、CVE は攻撃そのものではない。 CVE は、あとから識別子を与えられた一点だ。
攻撃者は CVE を「対応済みか未対応か」の一覧として見ていない。次の入口、次の権限、次の横展開、次の配布経路へ進むための部品として見ている。
codfish でも同じことが起きている。
タグを書き換える。 CI で動かす。 シークレットを抜く。 正規パッケージを公開する。 下流が install する。
一つひとつは別の弱点に見える。だが攻撃者にとっては一本の攻撃経路だ。
これはネットワーク機器でも同じだ。
古い VPN、変換器、NAS、Wi-Fi ルータ、監視カメラ。守る側は「この機器にこの CVE がある」と個別に処理しがちだ。だが攻撃者にとっては、内部ネットワークへ入る入口であり、認証情報に近づく踏み台であり、次のシステムへ横展開するための通路だ。
CVEを点で見るか。 攻撃経路として見るか。
この差が、守れるかどうかを分ける。
もちろん、点の検査は必要だ。
SCA は依存パッケージを見る。 CSPM はクラウド設定を見る。 シークレットスキャナは漏洩を見る。 EDR は端末を見る。 SBOM は構成要素を見る。
どれも必要だ。だが、それぞれが見ているのは多くの場合、点だ。
攻撃者が見ているのは、それらの間にある線だ。
このパッケージの脆弱性は、どの CI 権限につながるのか。 この GitHub Action は、どのシークレットに触れるのか。 この古い VPN は、どの内部システムへ届くのか。 この npm token は、どのパッケージを公開できるのか。 このパッケージを汚染すると、どの利用者へ届くのか。
本当に見るべきなのは、このつながりだ。
そこまで一貫して見せてくれるツールは、まだ少ない。だから守る側は、問いを変える必要がある。
「この CVE に対応したか」では足りない。 「この CVE は、どの攻撃経路の一部になりうるのか」まで見る。
「このパッケージは危険か」では足りない。 「このパッケージが侵害されたら、どの権限と利用者に届くのか」まで見る。
「この機器に脆弱性があるか」では足りない。 「この機器から入られたら、どこまで進まれるのか」まで見る。
これが、脆弱性を攻撃経路として見るということだ。
まず点を潰す。これは必要だ。
codfish を使っていないか確認する。該当タグを参照していないか確認する。侵害ウィンドウ中に CI が動いていたなら、シークレットを失効し、再発行する。外部 GitHub Action はタグではなくフルコミット SHA で固定する。
だが、それで終わらせない。
次に、面を見る。
CI にどのシークレットを渡しているのか。 そのトークンはどのレジストリに公開できるのか。 そのパッケージは誰に使われているのか。 install 時に悪性パッケージを止める仕組みはあるのか。 セルフホストランナーや開発端末に、後続命令を受け取る余地はないか。 未管理のネットワーク機器から、同じ認証情報や内部環境に届かないか。
守る対象は、個別の CVE ではない。 CVE と CVE、設定不備、過剰権限、CI/CD、レジストリ、開発端末、ネットワーク機器がつながった攻撃経路だ。
点を潰すだけでは、面は守れない。 CVE も、面で分析して初めて攻撃に備えられる。
codfish は、今回の攻撃面の中でたまたま見えた一本のくさびにすぎない。
同じマーカーは npm にも、PyPI にも、別のリポジトリにも見つかっている。攻撃者は「npmの話」「GitHub Actionsの話」「PyPIの話」と分けて考えていない。配布経路、認証情報、実行環境がつながる場所を探しているだけだ。
だから、codfish を使っていないあなたも無関係ではない。
次に汚染されるのは、今日 install するパッケージかもしれない。 次に踏み台になるのは、誰も棚卸ししていないネットワーク機器かもしれない。 次に盗まれるのは、CI に置きっぱなしの公開トークンかもしれない。
CVEを点で見るな。 攻撃経路として面で見ろ。
相手が面で来るなら、こちらも面で守るしかない。
各リポジトリで、以下を確認する。
grep -R "codfish/semantic-release-action" .github/workflows
該当がある場合、@v2、@v2.2.1、@v3〜@v3.5.0、@v4、@v4.0.0、@v4.0.1、@v5、@v5.0.0 を参照していないか確認する。
侵害は2026年6月24日以降に発生している。StepSecurity は協定世界時15:39:06頃と報告しているが、時刻を使う場合は原典で確認する。
これ以降に該当タグを参照したワークフローが実行されていた場合、CI 上で利用可能だったシークレットは露出した前提で扱う。特に確認すべきものは以下。
GITHUB_TOKENNPM_TOKEN| 種別 | 値 |
|---|---|
| 悪性コミット | 5792aba0e2180b9b80b77644370a6889d5817456 |
| 悪性コミット | bcb6b1d409144318e8fad2171d6fe06d02299d1a |
index.js の SHA-256 | 9f93d77d32833a515bc406c46da477142bb1ac2babeecb6aa42f98669a6db015 |
| マーカー文字列 | thebeautifulsnadsoftime |
| Bun ランタイム導入元 | oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 |
ただし SHA 固定は入口を狭める対策であって、盗まれた場合の被害範囲を小さくする対策ではない。CI に渡す token は最小権限・短寿命・用途別に分け、publish 権限を持つ token を常時置きっぱなしにしない。
Aikido Community Japan
公開日: 2026/6/25
最終更新: 2026/6/25
セキュリティの最新情報をお届けします。