npm/yarn/pnpm/bunを同じコマンドで扱えるni.zshに、npmで配布されているマルウェアを間違ってインストールするのを防ぐ機能を追加しました。

ni.zshについては、次の記事を参照してください。

npmパッケージのマルウェア

npmパッケージとしてマルウェアをpublishして開発者を狙うサプライチェーン攻撃が最近多くなっています。

たとえば、次の記事ではemails-helperというもっともらしいパッケージ名でマルウェアが配布されていました。

他にもtyposquattingという有名なパッケージ名のtypoを狙ってマルウェアを配布しているケースも多いです。

npmは利用者が多いため、こういったマルウェアのパッケージが月に数百件とかの単位で増えています。

最近では、1つのパッケージではなく、複数のパッケージを組み合わせて発動するようなマルウェアもあります。 これは、検知の回避などが目的だと思いますが、攻撃がどんどん複雑になってきています。

現状のマルウェアの多くはローカルの環境変数やAPIキー、秘密鍵などを盗むようなものが多いです。 そのため、これらを空振りさせることも一つの対策で、それについては次の記事で書きました。

こういったマルウェアパッケージはpostinstallなどのインストール時にスクリプトを実行する仕組みを使って、攻撃を行うことが多いです。(普通にrequireされた時に動くものがあります) そのため、--ignore-scriptsなどでpostinstallを実行しないようにすることで、一定の攻撃を防ぐことができます。 ただ、--ignore-scriptsはまだデフォルトにするのが難しい部分があります。

そもそもとして、こういったマルウェアっぽいパッケージをインストールしないようにするのが一番です。 しかし、パッケージ名を間違って入れてしまったりすることもあるので、これを意識して防ぐのは難しいです。

最近もrss-parserというパッケージを入れようとして、コピペしたらrss-parseになっていて、別のパッケージがインストールされそうになったことがあります。

この時は、今回紹介するni.zshに追加したマルウェアっぽいパッケージを検知する機能で防ぐことができました。 (実際にはrss-parseはマルウェアではないですが、全く別のパッケージで依存関係に問題があったのでインストール前に気づけたのはよかった)

ni.zsh + socket.dev

ni.zshのマルウェア検知機能

ni.zshのマルウェア検知機能は、Socketというサービスを利用しています。

Socketは、npmなどのサプライチェーン攻撃を検知するサービスで、特定のパッケージのリスクスコアやパッケージの中身を分析した結果を返してくれます。

たとえば、先ほどの記事でも登場したemails-helperというマルウェアパッケージの分析結果は次のページで見られます。

email-helper

AIベースのリスク評価postinstallでインストール時に何か実行するリスクバージョン間で大きな変更が入っているリスク特定のネットワークリスクのリスクなどがあることがわかります。

これらの結果や総合的なリスクスコアが、API経由でも取得できるので、これを使ってni add <package>でパッケージを追加するときに、マルウェアっぽいパッケージを検知することができます。

先ほどの、rss-parserrss-parseとtypoしたケースも、typosquattingのリスクが検知され、実際にはインストールする前に気づくことができました。 Socketでは、著名なパッケージとよく似た名前のパッケージのダウンロード数からtyposquattingの判定しているようです。(必ずしも問題があるパッケージではないが意図しないパッケージの可能性が高い)

ni.zshのマルウェア検知機能の使い方

この機能を使うには、SocketのAPIキーが必要です。 次の手順でAPIキーを取得してください。

  1. https://socket.dev/ にアクセスして、アカウントを作成します
  2. プロジェクトを作成します(適当なリポジトリにGitHub Appをインストールするだけです、どのリポジトリでもOKです)
  3. https://socket.dev/dashboard/org/gh/{user}/settings/api-tokens にアクセスして、APIキーをコピーします

取得したAPIキーを、NI_SOCKETDEV_TOKENという環境変数に設定すると、自動的にni.zshのマルウェア検知機能が有効になります。

export NI_SOCKETDEV_TOKEN="<socket-token>"

基本的には、APIで取得できるリスクスコアの値が一定以上の場合に、マルウェアっぽいパッケージとして検知します。 マルウェアっぽいものと判定された場合は、インストール前に本当にインストールするかの確認を求められます。 複雑なことをやってるパッケージは誤検知されることが多い(bundleされているやUnicodeの特殊な文字を使ってるなど)ので、実際に確認してからインストールするかを判断すると良いと思います。

詳しい使い方はREADMEを参照してください。

おわりに

Socket自体も同じようなことをするnpm コマンドのラッパーを提供しています。

ただこれは、npmしか対応していないので、yarnpnpmなどのパッケージマネージャーを使っている場合には使えません。

ni.zshは、npm/yarn/pnpm/bunを同じコマンドで扱えるので、これらのパッケージマネージャーを使っている場合にも使えます。 元々、プロジェクトごとにパッケージマネージャーが違う場合にその違いを意識せずに使えるようにするためにni.zshを作りました

今では、普段使うようなコマンドは、大体ni.zshで使えるようになっています。

ni                      -- install current package.json
ni add <pkg>            -- add package
ni remove <pkg>         -- remove package
ni run <script>         -- run scripts
ni test                 -- run test script
ni upgrade              -- upgrade packages
ni upgrade-interactive  -- upgrade package interactively
ni exec <command>       -- execute command
ni dlx <pkg>            -- download package and execute command

仕組み的にただのパッケージマネージャーのラッパーなので、こういったマルウェア検知機能を追加することができてよかったです。