利用しているnpmモジュールにセキュリティ問題があるときの対処


JavaScript/EcmaScriptでプログラムをする上で、npmモジュールはとても強い味方です。
ですが、時に使用しているモジュールに脆弱性が発生して、セキュリティ上の問題が生じることもよくあります。
もちろん最大の問題は脆弱性が悪用されることですが、そこまでの事態は引き起こさない軽微な脆弱性でも、githubやnpmスクリプトで処理する際にセキュリティ警告が出て、うんざりさせられることは本当によくあります。


npmスクリプトで出るセキュリティ警告メッセージの例


Github上で出るセキュリティ警告メッセージの例

もしこれが、脆弱性のあるモジュール自身を自分のプログラムが直接使っているだけで、かつ脆弱性対策がすぐに打たれたりしたならば、対応は簡単です。
単に、自分のプログラムのpackage.jsonの中で、dependency内で当該モジュールのバージョンを対策済みのものに対応させるだけです。
最近はnpmnpm audit fixコマンド、githubのDependabot security updates機能のように、その簡単な作業すら、コマンド一発、GUI操作一発で自動でできるような仕組みもできており、とても便利になっています。

しかし、脆弱性のあるモジュールの作者が対策済み版を作ってくれなかったり、脆弱性のあるモジュール自体は対策されても、それが自分のプログラムで使っているモジュールの中で孫使いされていたような場合は、なかなかやっかいです。
そんな場合の対処法を本記事でご紹介したいと思います。

脆弱性のあるモジュールは対策済みの場合

脆弱性のあるモジュール(仮にモジュールaとします)が他のモジュールの依存性の中で使われている場合、他のモジュールの依存ツリーの中にあるモジュールaも全て対策済みのものに置き換える必要があります。
しかしながら、モジュールaを使っているモジュールが全部対策を打ってくれればいいですが、放置されてしまった場合、普通のやり方ではその依存ツリーの中のモジュールaを更新することはできません。

そういう時には、npm-force-resolutionsを使います。
たとえば、minimistというパッケージを1.2.5以上に固定したい時は、package.jsonの中に

package.json
{
  "scripts": {
    "preinstall": "npx npm-force-resolutions || echo 'ignore'"
  },
  "resolutions": {
    "minimist": "^1.2.5"
  }
}

のような記述を追加すれば、対応できます。
若干説明を加えると、resolutions属性の下に指定したいモジュールとそのバージョンを指定し、scriptspreinstallnpx npm-force-resolutionsを加えると、依存ツリーの奥まで同一バージョンで統一してくれます。

その後についている|| echo 'ignore'について説明しますと、作っているプログラムが最終成果物であればこれはいらないです。
が、もし作っているプログラムがそれ自体npmに登録されるようなモジュールである場合、npx npm-force-resolutionsはエラーを返してしまうので、それを無視するため|| echo 'ignore'が必要になります。
ちなみにこの|| echo 'ignore'はMac、Windowsで動くことは確認していますが、WindowsのPowerShellでは動かないそうです。
完全にクロスプラットフォームにするにはまだ工夫が必要そうです。

また、当然ながら、これは当然バージョンアップに伴うインタフェースの更新などには対応していませんので、インタフェースなどが変わっていれば当然エラーが発生します。
その辺にも対応するには、この後に紹介する技術などを組み合わせて対応する必要があります。

脆弱性のあるモジュールが修正未対応の場合

もし、脆弱性のあるモジュール自体が未修正の場合、自分で修正する必要があります。
修正の方法は、自分でプログラムするなり、元モジュールの方で放置されているプルリクエストを適用したりと色々だと思いますが、そういったものを適用する際に役立つ方法を紹介します。

自分で修正したモジュールを修正前のモジュール名と同じ名前で、依存ツリーを遡って適用するには、主に以下の3通りがあり得ます。

  • 修正をGithubにアップロードし、Githubからインストールする場合
  • 修正をローカルフォルダにダウンロードし、ローカルのzipからインストールする場合
  • 修正を別名でnpmパッケージとして公開し、そこからインストールする場合

それぞれについて、

  • 修正済みプログラムをnpmコマンドを使って元モジュールの名前空間でインストールする方法
  • 依存ツリーまで遡って適用するためのpackage.jsonの書き方

を紹介します。

なお、それぞれのやり方は脆弱性を出しているモジュールがdevDependencies側のツリーにあると言う前提で書いています。
dependencies側にある場合は、適宜--save-devsavedevDependenciesdependenciesと読み替えてください。
また、例として、実例であるutils-extend(Github, npm)の1.0.8の驚異大の脆弱性に対し、私がフォークしてプルリクエストを充て(Github)、npmにutils-extend-patchedとして公開している事例を使っています。

修正をGithubにアップロードし、Githubからインストールする場合

npmからインストール
$ npm install --save-dev utils-extend@kochizufan/utils-extend
package.jsonを修正して依存ツリーにまで対応

自分で修正したパッケージを別名を使ってインストールする場合、scripts/preinstallresolutionsの変更だけでは不十分で、devDependenciesの中でも脆弱性対応のモジュールを加えておく必要があるようです。

package.json
{
  "scripts": {
    "preinstall": "npx npm-force-resolutions || echo 'ignore'"
  },
  "devDependencies": {
    "utils-extend": "github:kochizufan/utils-extend"
  },
  "resolutions": {
    "utils-extend": "github:kochizufan/utils-extend"
  }
}

修正をローカルフォルダにダウンロードし、ローカルのtarballからインストールする場合

npmからインストール

tarballは同じ階層のフォルダにutils-extend-1.0.9.tar.gzとして置かれているものとします。

$ npm install --save-dev utils-extend@file:utils-extend-1.0.9.tar.gz
package.jsonを修正して依存ツリーにまで対応
package.json
{
  "scripts": {
    "preinstall": "npx npm-force-resolutions || echo 'ignore'"
  },
  "devDependencies": {
    "utils-extend": "file:utils-extend-1.0.9.tar.gz"
  },
  "resolutions": {
    "utils-extend": "file:utils-extend-1.0.9.tar.gz"
  }
}

修正を別名でnpmパッケージとして公開し、そこからインストールする場合

npmからインストール
$ npm install --save-dev utils-extend@npm:[email protected]
package.jsonを修正して依存ツリーにまで対応
package.json
{
  "scripts": {
    "preinstall": "npx npm-force-resolutions || echo 'ignore'"
  },
  "devDependencies": {
    "utils-extend": "npm:utils-extend-patched@^1.0.9"
  },
  "resolutions": {
    "utils-extend": "npm:utils-extend-patched@^1.0.9"
  }
}

これらの手法のどれかを使えば、対策の取られていない脆弱性のあるモジュールに独自に手を加えて、依存ツリーまで含めて、githubやnpmスクリプトで処理する際にセキュリティ警告でなくすることができます。

注:脆弱性が本当になくなっているかまでは確認していません

以上、githubやnpmスクリプトでレポートされた脆弱性に対し、それらでセキュリティ警告が出なくなる手法をお伝えしました。
が、実際に動作の中で依存ツリー内のモジュールが全て対策済みのモジュールを使っているかはよくわかりません...確かめる方法も私は思いつかないので、本来の意味での脆弱性がこのやり方で解決しているかは不明です。
とはいえ、とりあえず脆弱性をチェックするスクリプトで脆弱性があると出てこないだけでも、マルウェアなどの攻撃対象にされる可能性も減るのではないかと思いますので、紹介させてもらいました。