- オープンソースパッケージ @ctrl/tinycolor を含む複数の npm パッケージが悪性バージョンに感染する事件が発生し、原因は共同リポジトリの GitHub Actions ワークフローを通じた npm トークンの窃取 だった
- 攻撃者は広範な権限を持つ npm トークンを使って約 20個のパッケージ に悪意あるコードを配布し、このうち @ctrl/tinycolor は週間ダウンロード数が 200 万回に達していたため影響が大きかった
- 感染したバージョンは postinstall 段階で悪性ペイロードを実行し、GitHub および npm セキュリティチームが迅速に対応して削除と整理措置が行われた
- 筆者は再発防止のため、Trusted Publishing(OIDC) への移行、トークン権限の最小化、2FA の義務化、pnpm 機能の活用など、強化されたセキュリティ計画を用意した
- 今回の事件は ソフトウェアサプライチェーンセキュリティの脆弱性 を示しており、npm エコシステム全体でのセキュリティ機能改善とセキュリティ慣行の変化の必要性を浮き彫りにした事例である
TL;DR
- 悪性の GitHub Actions ワークフローが共有リポジトリに push され、npm トークン が窃取された事件
- そのトークンを使って攻撃者は 20個のパッケージの悪性バージョン を配布し、その中でも @ctrl/tinycolor はダウンロード数が多く波及効果が大きかった
- 個人アカウントやリポジトリが直接侵害されたわけではなく、フィッシングやローカルへのマルウェア導入もなかった
- GitHub/npm セキュリティチームの迅速な対応により悪性バージョンは削除され、その後クリーンなバージョンが再配布されてキャッシュも整理された
事件発覚の経緯 (How I Found Out)
- 9月15日の午後、コミュニティメンバー Wes Todd が Bluesky の DM を通じて問題を知らせてくれた
- すでに GitHub/npm セキュリティチームは影響を受けたパッケージの一覧を整理し、削除対応を開始していた
- 初期の手がかりとして「Shai-Hulud」という悪性ブランチ名が共有され、これは Dune 世界観の サンドワーム の名称に由来する
実際に起きたこと (What Actually Happened)
- 以前に共同作業していた angulartics2 リポジトリ に、いまも admin 権限を持つ協力者が残っていた
- そのリポジトリに保管されていた npm トークン が悪性 GitHub Actions ワークフローによって窃取された
- 攻撃者はこのトークンで @ctrl/tinycolor を含む約 20 個のパッケージを配布した
- GitHub/npm セキュリティチームが素早く悪性バージョンを削除し、筆者が信頼できる新しいバージョンを再配布した
影響 (Impact)
- 悪性バージョンをインストールすると postinstall スクリプト が実行され、セキュリティ上の脅威が生じる
- 影響を受けたユーザーには、StepSecurity の 即時対応ガイド を参照することが推奨される
配布環境と対応計画 (Publishing Setup & Interim Plan)
- これまでは semantic-release + GitHub Actions の組み合わせで自動配布を行っていた
- npm の provenance 機能を活用していたが、有効なトークンを持つ攻撃者を防ぐことはできなかった
- 今後は Trusted Publishing(OIDC) の導入により静的トークンを廃止する計画
- 現在はすべてのトークンを失効させ、2FA の必須化、granular 権限のトークンのみ許可、pnpm の minimumReleaseAge 機能の検討など、追加のセキュリティ対策を適用している
理想的な改善案 (Publishing Wishlist)
- npm アカウント単位で OIDC ベースの Trusted Publishing を強制するオプション の提供が必要
- provenance が欠落している場合に配布をブロックする機能、semantic-release と OIDC の完全統合サポートが必要
- GitHub UI で 2FA ベースの手動承認配布 機能が提供されることを希望
- Pro サブスクリプションがなくても GitHub Environments レベルの保護機能を利用できるようにすべき
- npm パッケージページで postinstall スクリプトの有無表示 と削除されたバージョンの理由公開が必要
1件のコメント
Hacker News の意見
そのリポジトリには依然として GitHub Actions シークレット、つまり広範な公開権限を持つ npm トークンが残っていた
Trusted Publishing の利点のひとつは、長期有効な公開トークンをもう使う必要がないこと
今は CI VM 上で短期的に生成されるトークンだけを使い、トークンの有効期限も 15 分しかない
すでに PyPI、npm、Cargo、Homebrew など複数のエコシステムで導入されている
公開プロセスが実際に少し楽にもなるので、みんな試してみることを勧めたい
ドキュメントがまだ分かりにくいと感じるなら、いつでも助けを求めてよい
エコシステム管理者たちはこの機能が広がることを強く望んでいる雰囲気だ
Trusted Publishing 公式ドキュメント
npm で Trusted Publishing ができるようになっていたことを今回初めて知った
関連ニュース
今週末にさっそくセットアップしてみる予定
こういう機能を使っているプロジェクトだとリポジトリに表示するフラグがあるとよいと思う
そうすれば、それを使っていない依存パッケージを簡単にブロックできる
自動デプロイのプロセスに MFA(多要素認証)を含めるという論点が十分に注目されていない気がする
CI ワークフローで publish しつつ、MFA プロンプトで公開を確定するのは問題ないが、以前見たときはコード提供のために HTTPS トンネルを開く必要があり複雑だった
npm や GitHub が、CI 中に MFA コードを簡単に提示・承認できる方法をすぐに提供してくれればよいのだが
パッケージ公開には 2 つの段階がある: パッケージを npmjs にアップロードする段階と、実際にユーザーへ公開する段階だ
現在はこの 2 段階がひとつの作業に束ねられてしまっている
自分としてはこれを分離して、CI システムには自動でビルドとアップロードだけさせ
アップロード済みパッケージを本当に配布するには、人が npmjs の Web サイトに直接ログインして手動で publish と MFA を行う構造が妥当だと思う
そもそもパッケージ publish 自体が不要な概念なのではとも思う
VCS が「本当のソース」なら、別個の publish プロセスなしにそのまま使えばよいのではないか
Go 言語は実際にその方式だ
URL ベースで直接 import し、バージョニングもタグで管理する
こうすれば VCS だけを信頼すればよいので、追加の攻撃面が減る
アーカイブファイルを別途 diff する必要もなく、コミット単位だけ確認すればよい
問題はリポジトリを移動すると import パスが変わることだが、これも一種の利点と見なせる
それ以外に、別個の publish 段階がもたらす利点が何なのかよく分からない
昔 FTP で tar アーカイブをアップロードしていた時代の遺物のように感じる
以前 angulartics2 という共有リポジトリを触っていた
そこには今でも広範な公開権限を持つ npm トークンを含んだ GitHub Actions シークレットがあった
ある協力者が複数プロジェクトの権限を持っていたこともあり、それが複数パッケージが同時に影響を受けた理由だと推測している
Shai-Hulud という新しいブランチが、悪意ある github action workflow とともに force push された
管理者権限を持つ協力者だったため、レビューなしで即座にワークフローが実行され npm トークンが漏えいした
漏えいしたトークンにより 20 個のパッケージに悪意あるバージョンが配布された
その多くはあまり広く使われていないパッケージだが、@ctrl/tinycolor は週に約 200 万回ダウンロードされる人気パッケージだ
まだ理解できないのは、angulartics2 リポジトリの npm トークンでどうやって tinycolor まで publish できたのかという点だ
自分も他人の npm リポジトリに管理者権限があり、最近のリリースもほとんど自分が行っている
管理者になってから、長いあいだ放置されていた問題をこの機会に直したくなって、自分名義のコミットも増えた
GitHub Actions でパッケージを publish する方向にほぼ気持ちは傾いていたが、2FA で手動デプロイした場合に、誤って master 以外の状態を公開してしまわないかと常に不安だった
こうした問題のせいで他の管理者と話し合うのも先延ばしにしていたが、結局こうなったのを見ると、先延ばしにしてよかった気もする
正解が何かは分からないが、認証情報を第三者に預けるのは確実によい答えではない気がする
説明不足だったならすまない
このトークンは、自分の npm パッケージ全体に対するグローバルな公開権限を持つトークンだった
ここ 10 年ほど手動リリースをずっと主張してきた
いつも反発は多かったが、最近ではそこまで奇妙な考えでもないようだ
CI/CD が便利なのは分かるが、今回の件や最近の CF の問題を見ると、むしろ自動化によって深刻な問題が起きやすくなる証拠が増えている
以前 BigBank で働いていたときは、本番デプロイに少なくとも 5 人が待機し、多くの手順を踏んでいたが、それでも何をデプロイするのかは確実に把握できていた
GitHub Actions や自動リリーススクリプトのせいではなく、昔のように直接ビルドして署名し、tarball を上げて検証する方式のほうがはるかに安全だと思う
配布システム(たとえば Debian のようなディストリビューションのパッケージングシステム)は別個の検証段階を持つこともあり、xz の件でインターネット全体がハックされなかった理由でもある
少なくともリリースが publish される前に、人が直接バイナリへ署名する手順を必須にすべきだ
攻撃者が自分自身をメンテナーに追加して自分の鍵で署名する問題もあるので、ディストリビューションのパッケージングシステムのように信頼された鍵管理も併用されるべきだ
もし自分の脅威モデルが「GitHub アカウントや API キーが 1 つでも漏れれば全ユーザーが突破される」に基づいているなら、本当に合理的か自問すべきだ
publish に 2FA を使うのもよいが、複数の作者が暗号署名で同意するほうがずっと安全だ
1 人だけ突破されても攻撃が成功しないようにすべきだ
多くのパッケージは作者が 1 人しかいない
複数作者の署名を要求するのもよいが、コミット、タグ、成果物などに何らかの署名検証が行われるなら、大半の攻撃は防げる
ディストリビューションのパッケージングは署名検証の支援が徹底しているが、言語パッケージマネージャーにはその検証体系が不足している
例として runc の公式リリース工程はすべてメンテナーの鍵で署名され、鍵は Yubikey などに保管されている
ディストリビューションのシステムも別個のキーリングを管理しつつ、公式ソースとバイナリを検証する
もしこうした工程があれば、今回の攻撃も複数段階で遮断できたと思う
CI でそのままビルドすることはできても、最終的にはメンテナーが直接署名する構造が必要だ
言語パッケージマネージャーにこうしたワークフローがないなら、Trusted Publishing はまだしも比較的ましな代替案ではある
ただし GitHub アカウントが攻撃されると(たとえば Cookie 盗難など)、すぐに publish できてしまう
GitHub では Trusted Publishing にタイムアウトなどのセキュリティ設定を用意しているが、攻撃者が無効化できる可能性もある
自分のアカウントが侵害されても、ディストリビューション側では自分が署名していない鍵による変更は受け入れられないので、相対的には安全だ
参考までに、自分は SUSE 所属だが、openSUSE、Arch、Gentoo のように成果物検証の支援がさらに広がってほしいと思っている
関連リンク:
runc.keyring
keyring_validate.sh
release_sign.sh
openSUSE の runc.keyring
自分はトークンが大嫌いだ
トークンはただの静的パスワードと同じだ
もっとまともな認証方式が必要だと思う
例として、GitHub を AWS のトークンプロバイダーとして使う方式がまだしも望ましい
GitHub-AWS OIDC 統合方式
ただしこの方式は例外的なケースにすぎない
マシン間の OIDC フローは、きちんと実装すれば安全になり得るが、設定が複雑すぎる
そして結局 OIDC も「より複雑なトークン」にすぎない感じがある
人が直接確認しない自動化環境では、常にどこかに漏えいし得る何かが残る(トークンであれトークン生成器であれ)
今回のワーム事例でも、OIDC は根本解決ではなかった
GitHub ワークフローが破られていたなら、OIDC かどうかに関係なく環境へ一時的なアイデンティティが注入されるだけだ
結局のところ、未承認ユーザーがシークレットを持つワークフローを実行できない仕組みが重要だ
細かく権限を分けたいなら、OIDC よりむしろトークン権限のスコープを狭めるほうが効果的かもしれない
トークンの本来の趣旨は、有効期間と権限範囲(authZ)が制限されることだ
ところがほとんどの場合、実際にはそうなっておらず、ただのパスワードのように静的に使われている
oauth や、詳細な権限制限が可能な biscuits のような代替もあるが、実際にはあまり使われていない
Trusted Publishing は今や npm など複数のパッケージレジストリでサポートされている
関連ニュース
他の人も触れていたが、トークンは短寿命であるか、手動認証(MFA、パスフレーズなど)の後でのみ発行されるべきだ
mTLS(TLS クライアント証明書)を使うのが正解に近い方向だ
脆弱な npm パッケージのチェック用に公開されたツールやスクリプトをご存じの方はいるだろうか?
stepsecurity のページにはそういうツールがないように見える
すべてを防げるわけではないが、provenance-action を導入するのもよいアイデアだ
provenance-action
既知の問題については
npm auditを使うのが基本だ自分が言いたかったのは、ローカル publish で間違ったブランチやビルド漏れなどのミスが少し心配だったという意味だ
もし CI ジョブが git 履歴の深い部分を force push で変更したらどうするのか、と思う
現状(status quo)はもはやまともに機能していない
もちろん OIDC トークンやゼロトラストソリューションなどの技術的な利点を称賛することはできるが
数百万ダウンロードされる npm ライブラリのメンテナーのかなりの部分は、実際にハックされるか npm が配布停止をかけるまで、セキュリティに気を配らないだろう
そして「依存関係を全部なくして標準ライブラリだけにしよう」のような実現不可能な主張まで出てくる
依存関係を減らすのはよいことだが、すでに存在する問題に対しては何の解決にもならない
現実的には、1 万人や 10 万人が npm を離れてみなコードを書き直すか、逆に npm がダウンロード数の多いパッケージを中心に 2FA や OIDC のような規則を強制し、守らなければ配布自体を止めるか、そのどちらかしかない
どちらが現実的に実行可能かは明らかだ
そうでなければ npm の評判は地に落ち、XKCD 927 の状況になるだけだ