event-stream 2018年 引き継がれた部品への悪いコードの混入
2年余り版の出ていなかった npm の部品の公開の権限が、メールで手を挙げた者に渡った。その者が出した修正版に新しい依存が1つ足され、その依存が特定の財布のアプリのビルドの中でだけ秘密の鍵を盗むコードを差し込んだ。範囲指定で版を受け入れる側は、何もせずにその版を取り込んだ。
要点
- 元の保守者は、保守したいとメールしてきた者に部品を渡した。自分はもう使っておらず、保守しても得る物が無い、と書いている。
- 新しい保守者は、修正の版 3.3.6 で flatmap-stream を直接の依存に足した。npm の登録所は、誰が公開しても正規の名前の版として配る。
- 悪いコードは、試験の資料に見せかけたファイルに暗号化して置かれ、取り込んだ側のプロジェクトの説明文を鍵にして解けた。財布のアプリ Copay のリリースのビルドの中でだけ働いた。
- 範囲指定
~3.3.0で頼る部品は、3.3.6 を自動で取り込んだ。同じ週に出た 4.0.0 には、この依存は無かった。 - 根本の守りは、引き継ぎを手続きにする、版を固定し差分で読む、ビルドの成果物の差を見る、の3つ。
何が起きたか
| 日付 | 出来事 |
|---|---|
| 2016年7月17日 | event-stream 3.3.4 が出る。これより後、2018年9月まで版が出ない[4] |
| 2018年9月5日 | 3.3.5 が出る[4] |
| 2018年9月9日 | 3.3.6 が出る。新しい保守者が flatmap-stream を直接の依存に足した[1][4]。悪い版は flatmap-stream 0.1.1[1] |
| 2018年9月16日 | 4.0.0 が出る[4]。GitHub Advisory はこれを直った版とする[3] |
| 2018年11月20日 | 利用者の1人が event-stream のリポジトリに issue を立て、flatmap-stream 0.1.1 に暗号化され難読化された中身があると報告する[2] |
| 2018年11月22日 | 元の保守者が issue に返信する。保守したいとメールしてきたので渡した、自分はもう何年も使っていない、npm の公開の権限はもう持っていない、と書く[2] |
| 2018年11月26日 | 朝、npm のセキュリティの担当が知らせを受け、flatmap-stream と event-stream 3.3.6 を登録所から消し、event-stream の管理を引き取る[1]。GitHub Advisory が公開される[3]。event-stream に頼る ps-tree が、依存を 3.3.4 ちょうどに固定した版を出す[5] |
| 2018年11月27日 | npm が解析を公表する。狙いは Copay で、悪いコードは Copay の 5.0.2 から 5.1.0 に入ったと Copay が確かめた、と書く[1] |
公表資料に無い数字
報道では、週ごとや期間ごとの取り込みの回数、盗まれた額が語られることがある。npm の解析は event-stream を広く使われている部品と書くだけで、回数を示していない[1]。被害の額は、npm も GitHub も公表していない。
通った道筋
| 段 | 何があったか |
|---|---|
| 引き継ぎ | npm は、保守者を装った者が社会工学で保守を引き取ったと書く[1]。元の保守者は、メールで頼まれて渡したと書き、相手を確かめた記述は無い[2]。 |
| 公開 | 3.3.6 は、版の数字の上では修正の版である。新しい保守者が flatmap-stream を直接の依存として足した[1]。登録所は、公開の権限を持つ者が出した版を、そのまま正規の版として配る。 |
| 依存の解決 | たとえば ps-tree は、1.0.0 から 1.1.0 まで event-stream を ~3.3.0 で指定していた[5]。この範囲は 3.3.6 を含み、4.0.0 を含まない。ロックファイルを作り直すか持たない利用者は、悪い版だけを受け取る。 |
| 中身 | 暗号化した資料が、試験の資料に見せかけたファイルに隠されていた。取り込んだ側のプロジェクトの説明文を鍵にして解き、メモリの中で組み立てて走らせた[1]。説明文が合わない所では解けず、何も起きない。 |
| ビルド | Copay の開発者がリリースのビルドの台本を走らせたときに、2段目のコードを、ビルドが束に入れる依存へ書き込んだ[1]。 |
| 実行時 | 配られたアプリの中で、モバイル、Cordova、Electron のどこで動くかを見分け、ビットコインの残高が100 BTC を超えるか、ビットコインキャッシュが1000 BCH を超える財布で、口座の情報と秘密の鍵を外の集め先へ送った[1]。 |
狙いを絞ると、多くの利用者には何も起きない
npm は、狙った開発の環境の外ではこのコードは働かず、入れた開発者の多くは影響を受けなかったと書く[1]。何も起きないことは、安全の証しではない。走らせて振る舞いを見る点検は、鍵が合わない環境では何も見られない。中身を読めない暗号化した資料が依存に入ったこと自体を、止める理由にする。
仕組みの再現 架空の部品
悪い版は、コードの差分として読める所が小さい。package.json の依存が1行増えるだけで、悪い中身はその依存の中にある。架空の部品で、取り込む側から見える差分を示す。
取り込む側のロックファイルの差分 架空の部品 stream-util
"node_modules/stream-util": {
- "version": "2.4.4",
+ "version": "2.4.6",
"resolved": "https://registry.example.com/stream-util/-/stream-util-2.4.6.tgz",
"dependencies": {
"through": "~2.3.1",
+ "mapper-stream": "0.1.1" # 修正の版で、見たことの無い依存が増える
}
},
+ "node_modules/mapper-stream": {
+ "version": "0.1.1", # 公開から日が浅く、保守者は1人で、利用者が少ない
+ }
- 直接の依存の版の数字は変わらず、変わるのはロックファイルだけになることが多い。ロックファイルの差分を読まない運用では、誰も見ない。
- 見る所は、修正の版で依存が増えていないか、増えた依存の公開日と保守者と利用の数、元の部品の保守者が最近替わっていないか、の3つ。
どこで止められたか
| 止める所 | この件で | 限り |
|---|---|---|
| 権限を頼まれただけで渡さない | 元の保守者は、メールで頼まれて渡し、のちに npm の公開の権限を失っていた[2] | 保守を続けられない人に、続ける義務は無い。渡す代わりに、非推奨の印を付けて止める道が要る |
| ロックファイルで固定する | ~3.3.0 の範囲は 3.3.6 を含んだ[5]。固定していれば、ロックを作り直すまで届かない | 固定した版を上げるときに、同じ確かめが要る。固定は確かめる時を決めるだけ |
| 増えた依存を読む | 3.3.6 で直接の依存が1つ増えた[1] | 悪い中身は暗号化され、読んでも意味は取れない。読めない中身があることを止める理由にする |
| 配る束の差を見る | 2段目のコードは、ビルドの中で束に入る依存へ書き込まれた[1] | 自分のソースと依存の正しい版から作り直した束と比べる。ビルドの環境ごと汚れていれば差は出ない |
| 外への通信を絞る | 鍵は外の集め先へ送られた[1] | 送り先を許した相手に紛れさせる手口には効かない。鍵を端末の外へ出さない作りと組み合わせる |
守り方
根本の直し方
- 依存はロックファイルで固定し、CI は
npm ciで入れる。版を上げるのは、意図した時に、差分を人が読んでからにする。手順は AIが持ち込む部品の危険 の固定する、と足す差分で止めて確かめる、の節にある。 - 修正の版で依存が増えた、保守者が替わった、公開から日が浅い、のどれかに当たる版は、取り込む前に止めて調べる。型としては A03 ソフトウェアサプライチェーンの不備 の型2 悪意ある版が入る、に当たる。
- 保守する側は、続けられなくなった部品を、名乗り出た人へ黙って渡さない。非推奨の印を付けて止めるか、渡すなら利用者に告知し、引き継いだ人の実績を確かめる。
- 秘密の鍵を扱うアプリは、ビルドの成果物を、ソースと固定した依存から別の環境で作り直した物と比べてから配る。比べ方の考え方は xz utils 2024年 の束とタグの比較と同じ。
CI で入れる 架空のアプリ
npm install # 範囲の中の最新を取り直し、ロックを書き換え得る
npm ci --ignore-scripts # ロックどおりに入れ、食い違えば誤りで止まる
npm audit signatures # 登録所の署名と出どころを照合する
- 出どころの照合は、誰が出したかを確かめる手段で、中身の安全の証明ではない。この件の悪い版も、正しい公開の権限で出ていた。
- CI で導入時のスクリプトを止める理由と手順は A03 の型2にある。この件の中身は導入時のスクリプトではなく、読み込まれた時に走ったので、止めても防げない。
緩和の策
- 依存の木に入る部品の保守者の変化と、新しく現れた依存を見張る。公開から日の浅い版を入れない設定を使う。
- 財布のように秘密の鍵を持つアプリは、外への通信の相手を許可の一覧で決め、それ以外を通さない。
- 公に報告が出たら、ロックファイルで影響を受けた部品を洗い出し、影響を受けた版で作って配った物を特定する。npm は
npm auditで確かめ、影響を受けた版なら上げるよう求めた[1]。GitHub Advisory は 3.3.4 へ戻すか 4.x へ上げ、flatmap-stream は取り除くよう求める[3]。 - 影響を受けた版のアプリを使った利用者には、鍵を替えるよう知らせる。BitPay は、5.0.2 から 5.1.0 の Copay を開かないよう求めた[1]。
参照
- Details about the event-stream incident npm Blog、2018年11月27日
- I don't know what to say. dominictarr/event-stream issue 116、2018年11月20日から26日
- GHSA-mh6f-8j2x-4483 Critical severity vulnerability that affects event-stream and flatmap-stream GitHub Advisory Database、2018年11月26日公開
- event-stream の登録情報 npm registry、2026年11月10日に取得
- ps-tree の登録情報 npm registry、2026年11月10日に取得
- flatmap-stream の登録情報 npm registry、2026年11月10日に取得