event-stream 2018年 引き継がれた部品への悪いコードの混入

2年余り版の出ていなかった npm の部品の公開の権限が、メールで手を挙げた者に渡った。その者が出した修正版に新しい依存が1つ足され、その依存が特定の財布のアプリのビルドの中でだけ秘密の鍵を盗むコードを差し込んだ。範囲指定で版を受け入れる側は、何もせずにその版を取り込んだ。

GitHub Advisory GHSA-mh6f-8j2x-4483。重大度は Critical、CVSS 3.1 の基本値は9.8、CWE-506 埋め込まれた悪意あるコード[3]。影響は event-stream 3.3.6 と flatmap-stream の全版[3]。悪い版の公開は2018年9月9日、公の報告は11月20日、npm による削除は11月26日[1][2][4]。2026年11月時点で、event-stream の管理者は npm で、flatmap-stream の名前は npm の保全用の空の包みになっている[4][6]。2026年11月時点の公開資料で書いています。

要点

  • 元の保守者は、保守したいとメールしてきた者に部品を渡した。自分はもう使っておらず、保守しても得る物が無い、と書いている。
  • 新しい保守者は、修正の版 3.3.6 で flatmap-stream を直接の依存に足した。npm の登録所は、誰が公開しても正規の名前の版として配る。
  • 悪いコードは、試験の資料に見せかけたファイルに暗号化して置かれ、取り込んだ側のプロジェクトの説明文を鍵にして解けた。財布のアプリ Copay のリリースのビルドの中でだけ働いた。
  • 範囲指定 ~3.3.0 で頼る部品は、3.3.6 を自動で取り込んだ。同じ週に出た 4.0.0 には、この依存は無かった。
  • 根本の守りは、引き継ぎを手続きにする、版を固定し差分で読む、ビルドの成果物の差を見る、の3つ。

何が起きたか

2016年7月の 3.3.4 から2年余り版が無く、2018年9月に権限が移って 3.3.6 が出て、11月20日に報告、26日に削除された 1 止まった部品 2016年7月17日 3.3.4 を最後に 2年余り版が無い 2 権限が移る 2018年9月5日 3.3.5 が出る メールで頼んだ者へ 3 混ぜる 2018年9月9日 3.3.6 に新しい依存 flatmap-stream 4 見つかる 2018年11月20日 利用者が公に報告 26日に npm が削除 3から4まで約10週間。範囲指定の利用者には、その間ずっと配られた npm の登録所の公開の時刻と、npm と GitHub の記録による
図1 権限が移ってから見つかるまで
日付出来事
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 も公表していない。

通った道筋

公開の権限が新しい保守者に移り、3.3.6 に足した依存が範囲指定の利用者に届き、財布のアプリのビルドでだけ働き、配られたアプリが条件の合う財布の鍵を外へ送る 元の保守者 もう使っていない 頼まれて権限を渡す 新しい保守者 修正の版 3.3.6 を公開 依存を1つ足す flatmap-stream 0.1.1 暗号化した中身 利用する側の依存の木 アプリ 直接は頼らない 中間の部品 ~3.3.0 event-stream 3.3.6 に解決 Copay のリリースのビルド 説明文が鍵になり中身が解ける 配る束の依存へコードを書き込む 利用者の端末 残高が条件を超えると 鍵を外へ送る
図2 権限の移動から、利用者の端末で鍵が送られるまで
段何があったか
引き継ぎ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つ。

どこで止められたか

引き継ぎ、依存の解決、差分の確認、ビルド、実行時の5つの段に、それぞれ止める所を置く 引き継ぎ 依存の解決 差分の確認 ビルド 実行時 1 権限を頼まれただけで渡さない 止めるなら非推奨にし、登録所に引き取らせる 2 ロックファイルで固定する 範囲指定のまま新しい版を受け取らない 3 増えた依存を読む 修正の版で依存が増えたら、それだけで止める 4 配る束の差を見る 自分のコードに無い変更が束にあれば出さない 5 外への通信を絞る アプリが話す相手を決め、他を通さない
図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]。

参照

  1. Details about the event-stream incident npm Blog、2018年11月27日
  2. I don't know what to say. dominictarr/event-stream issue 116、2018年11月20日から26日
  3. GHSA-mh6f-8j2x-4483 Critical severity vulnerability that affects event-stream and flatmap-stream GitHub Advisory Database、2018年11月26日公開
  4. event-stream の登録情報 npm registry、2026年11月10日に取得
  5. ps-tree の登録情報 npm registry、2026年11月10日に取得
  6. flatmap-stream の登録情報 npm registry、2026年11月10日に取得