npmパッケージのvulnerability対応フロー


概要

  • npmプロジェクトで利用しているnpmパッケージ(依存パッケージ)でvulnerability(脆弱性)が見つかったときの対処フローについて記載します。

(GitHub等が親切に"We found potential security vulnerabilities in your dependencies."のように通知してくれるので便利)

対応フロー

ざっくり全体像は以下のとおり。

①最新のコードを取得する

いうまでもないが、手元のコードは最新のコードにしておく

git pull

最新であることが確認された

Already up to date.

ちなみに、今回は自作プロジェクト(https://github.com/riversun/simple-date-format) で実際にハンズオンした

②プロジェクトが使用しているnpmパッケージが最新かどうか確認する

脆弱性対応の前に、いま自分のプロジェクトが使っているnpmパッケージを最新のものにする。

プロジェクトが使っているnpmパッケージが最新かどうかはnpm outdatedコマンドで確認することができ、最新バージョンがある場合は、その情報を表示してくれる

npm outdated

するとこのように、最新版が存在するパッケージを一覧表示してくれる
いくつかのパッケージで最新版があるようだ

パッケージをアップデートする

package.jsonに記載されるnpmパッケージ一覧は以下のようになっている。
(「^」がついたキャレット表記についてはこちらで説明)

package.json抜粋
  "@babel/core": "^7.8.4",
  "@babel/preset-env": "^7.8.4",
  "babel-jest": "^25.1.0",
   ...

パッケージのバージョンの付け方セマンティックバージョニングに従っている。

セマンティックバージョニングにおいて、メジャーバージョン、マイナーバージョンは以下の意味をもっている。

これをふまえ、「パッケージを最新にする」には2種類の方法があるといえる。(パッチバージョンを上げるというのもいれれば3種類だが本筋にあまり影響ないので割愛)

  • ②-1 マイナーバージョンまで最新にする
  • ②-2 メジャーバージョンまで最新にする

②-1をとるか ②-2をとるかはポリシー次第だがメジャーバージョンを最新にする場合はAPIの後方互換が無いことを想定しておいたほうがいい。

②-1 マイナーバージョンまで最新にする場合

キャレット表記「^」つきで定義されたパッケージのバージョンを、マイナーバージョンまでを最新にするにはnpm updateコマンドで可能

npm update

以下のように、マイナーバージョンまでが最新になった
(ただし、メジャーバージョンは最新になっていない。)

+ [email protected]
+ [email protected]
+ [email protected]
+ @babel/[email protected]
+ [email protected]
+ @babel/[email protected]
+ [email protected]
added 36 packages from 17 contributors, removed 6 packages, updated 140 packages, moved 1 package and audited 263490 packages in 18.399s

②-2 メジャーバージョンまで最新にする場合

npm updateでは、マイナーバージョンまでしかアップデートしてくれなかったが、ここではメジャーバージョンまで容赦なくアップデートしてくれるパッケージnpm-check-updatesを導入する

以下のようにしてnpm-check-updatesをインストールする

npm install -g npm-check-updates

npm-check-updatesをインストールするとncuというコマンドが使えるようになる。

ちなみに、ncuコマンドだけをたたくと現在のパッケージバージョンと最新のパッケージバージョンを表示してくれるnpm outdatedコマンドのような動作をする

ncu

を実行すると、以下のように現在のバージョンと最新バージョンが表示される。

つづいて、ncu -uを実行すれば、上でみたとおりパッケージが最新バージョンになるようにpackage.jsonを書き換えてくれる

ncu -u

npm updateと違ってパッケージのインストールそのものはやってくれない。package.jsonが書き換わるだけなので、自分で npm install する必要がある

npm install

これでパッケージは最新になった

③npm auditで脆弱性のある依存パッケージを確認する

今、最新のパッケージにした状態だが、この状態でも脆弱性(valnerability)のあるパッケージが含まれていることがある。
npm auditコマンドを使えば脆弱性のあるパッケージを洗い出すことができる。

npm audit

をやってみたら、180個の脆弱性がみつかった。レベルはlow

found 180 low severity vulnerabilities in 263397 scanned packages
  run `npm audit fix` to fix 174 of them.
  6 vulnerabilities require manual review. See the full report for details.

④npm audit fixで自動修復をこころみる

npm audit fixをすると、脆弱性のあるパッケージのバージョンを自動的に脆弱性の無いバージョンに置き換えてくれる(努力をしてくれる)

npm audit fix

すると以下のようになった。
180個の脆弱性のうち174個が修正された。

removed 1 package and updated 2 packages in 4.641s

31 packages are looking for funding
  run `npm fund` for details

fixed 174 of 180 vulnerabilities in 263397 scanned packages
  6 vulnerabilities required manual review and could not be updated

残り6個はマニュアルレビューしてくれとかいてある。

⑤npm dedupeで重複したパッケージの整理統合をこころみる

npm dedupeを理解する

まず、dedupeの概念を理解するために以下の状態を考える

  • 自プロジェクトnpmパッケージAnpmパッケージBに依存している
  • npmパッケージAnpmパッケージC @2.1.1に依存している。
  • npmパッケージBnpmパッケージC @2.2.0に依存している。

この場合、npmを使っていると1npmパッケージC @2.1.1npmパッケージC @2.2.0もインストールされる。

パッケージとしてはnpmパッケージCで同じなのに、npmパッケージC @2.1.1npmパッケージC @2.2.0でバージョンが違いがあるので両方保持されしまう。

だったら、バージョンを新しいほうの@2.2.0のほうに合わせて、npmパッケージC @2.2.0のほうを、npmパッケージAnpmパッケージBで共通化して使いましょう、というのがdedupeの発想。

npm dedupeは新しいバージョンをインストールしてくれない

npm update等でdedupeはひととおりのパッケージを最新にした後にやる。
なぜなら、dedupe自身はパッケージの重複排除などはやってくれるが、最新のパッケージを入れてくれるわけではないので古いパッケージバージョンで状態でdedupeしてもあまり意味がない。

dedupeする

さて、ではnpm dedupe (npm ddpでもOK)してみる

removed 10 packages and audited 263298 packages in 3.622s
found 0 vulnerabilities

脆弱性が0になった!

なぜdedupeで脆弱性がゼロになった?

これは、dedupeが脆弱性を排除しているというより、古いパッケージバージョンに依存していたライブラリが新しいパッケージバージョンを参照するようにdedupeが変更してくれた効果のため。

上の例でいうと以下のようになる。

  • 自プロジェクトnpmパッケージAnpmパッケージBに依存している
  • npmパッケージAnpmパッケージC @2.1.1(脆弱性ありバージョン)に依存している。
  • npmパッケージBnpmパッケージC @2.2.0(安全バージョン)に依存している。

の状況をdedupeが↓のように変更してくれた効果

  • 自プロジェクトnpmパッケージAnpmパッケージBに依存している
  • npmパッケージAnpmパッケージC @2.2.0(安全バージョン)に依存している。
  • npmパッケージBnpmパッケージC @2.2.0(安全バージョン)に依存している。

⑦回帰テストを実行する

さてここまでで、脆弱性の無い状態にできたら、最後にテストをしてパッケージバージョンを変更したことによるプロジェクトのデグレが発生していないかどうかを確認する。

npm test

テスト無事通過!

(といっても、今回はdevDependenciesの依存のみだったけど)

これで対応完了!

途中でつまずいた場合

上述のコマンドだけでうまくいかず、つまずくことも多々ある。そうした場合は、コマンドだけで楽に突破できない状況になっている可能性があるので、それなりの調査分析工数を覚悟するしかない。

つまずき例

  • vulnerabilityが無くならなかった

    • 対応案:npm auditでvulnerabilityのあるパッケージに依存している上位のパッケージを特定する。そのパッケージがdeprecateになっていないか。ちゃんとメンテされているか確認する。deprecatedだったりメンテされていなければ使うのをやめたりPR送ってみたり、自分で直してみたり、代替パッケージを探したり。そういった工数を覚悟、確保する。
  • 最後のテストでつまづいた

    • 対応案:手順を1手ずつロールバック(手順を戻す)しながら、都度npm testを実行し、どの段階でつまづいたのか特定する。ありがちなのは②-1メジャーアップデートでAPIに破壊的変更

まとめ


  1. yarnは自動的にdedupeしてくれる(https://classic.yarnpkg.com/ja/docs/cli/dedupe/