直接依存していない内部で利用しているライブラリが脆弱性を抱えているときにyarnで手動対応する方法について

タイトルが長い。しかもパッとわからない。

ので以下のストーリーを理解されたい。

ストーリー

f:id:hitonatsu:20200823014747p:plain
脆弱性があると言われている

GitHubでセキュリティアラートをONにしていると依存しているライブラリに脆弱性があったときにこんな感じに教えてくれる。

そしてDependabotをONにしていると基本的に自動で脆弱性に対応するためのPRを作成してくれる。

ただなんでかたまにPRを作ってくれないときがあって自分でなんとかしないといけない時があり、そういうときは自分で依存モジュールを上げないといけない。

まあライブラリが対応さえしてくれていれば上げるだけでOKなのだが。

だがしかし

難しいのは脆弱性があるdependencyが直接依存しているモジュールじゃないとき。

例えば C というライブラリのバージョン"1.0.8"に脆弱性があるとする。

しかし自分のプロジェクトのpackage.jsonC は書かれておらず直接の依存はしていなくて C は 直接依存している A というライブラリが利用している B というライブラリの中でさらに依存しているというケース。

言葉で書くとわからん。

図にするとこう

/node_modules
├── A
│   └── node_modules
│       └── B
│           └── node_modules
│               └── C
├── react
├── dateFns
└── ...

Aの孫にCがある

ここで問題が出る

素直に考えれば直接依存している A脆弱性対応してくれるのを待つのだが、全世界すべてのライブラリが活発にメンテされてるわけでもないし、メンテされていてもすぐに対応されるとは限らない。

たとえばこういう風に A が対応してくれなかったとする

/node_modules
├── A < ぼくはBの"3.3.7"に依存しているけど、Bを"3.3.8"に更新対応するつもりはない
│   └── node_modules
│       └── B < "3.3.7"でCの"1.0.8"を使ってたけど、"3.3.8"でCの"1.0.9"に上げたので脆弱性は対応済みです
│           └── node_modules
│               └── C < "1.0.8"で脆弱性あったけど、"1.0.9"で対応済みです
├── react
├── dateFns
└── ...

😇 詰んでしまった……。

どうすればよいか

Dependabotが脆弱性対応のPRを投げてくれるときのPRを思い出してみる。

そういえばlockファイルをなんか書き換えてくれていて対応版のバージョンを指定している。

f:id:hitonatsu:20200823015842p:plain
Dependabotがやってくれるこういうlockファイルの部分更新を手動でやりたい

Dependabotと同じようにlockファイルをなんとかして手動で更新できれば上述でいうライブラリ A が対応してくれなくても C脆弱性対応してくれているのでなんとかなりそう。

つまり自分がDependabotと同じことをできればOK。俺たちはDependabotになりたい。

手順

やっとこさ本題。

yarnでは resolutions というフィールドに書かれたバージョンで強制的に利用するバージョンを固定するという技ができるので、それを用いる。

https://classic.yarnpkg.com/ja/docs/selective-version-resolutions/

ここからは例として "lodash": "4.17.15"脆弱性があり "4.17.19" で 対応されたとする。先ほどからのツリー構造でみるとこう。

/node_modules
├── A
│   └── node_modules
│       └── B
│           └── node_modules
│               └── lodash // "4.17.15"を使っていて脆弱性がある
├── react
├── dateFns
└── ...

上述してきたライブラリ C = lodash ということ。

1 package.jsonに問題のあるパッケージを書く

dependencies or devDependencies と resolutions に書く。

{
  "name": "example-project",
  "version": "1.0.0",
  "dependencies": {
    "lodash": "4.17.19" // もともとこのプロジェクトでlodashは直接使ってないが一旦入れる
  },
  "resolutions": {
    "lodash": "4.17.19" // このバージョンで固定する
  }
}

node_modules内に複数のメジャーバージョンがある場合は

resolutionsにパスとglobを書けるので複数のバージョン指定をそれぞれに対して行えばOK

Selective dependency resolutions | Yarn

 "resolutions": {
    "d2/left-pad": "1.1.1",
    "c/**/left-pad": "^1.1.2"
  }
2 lockファイルを更新

そしてインストールする。

$ yarn install

するとこんな感じのyarn.lockのlodashの欄にDiffが出る。

.
.
.
  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=

lodash@^4.11.1, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4:
  version "4.17.19"
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
  integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==

loose-envify@^1.0.0, loose-envify@^1.4.0:
  version "1.4.0"
.
.
.

Dependabotがやってくれるやつだ!

3 package.jsonを元に戻す

あとはさっき1で書いたpackage.jsonの内容を消す。

{
  "name": "example-project",
  "version": "1.0.0",
  "dependencies": {
  },
  "resolutions": {
  }
}

※ 実際にはこんな感じにまっさらではなく他のdepsは書かれている状況

なんで書いたものを消すかというと、あくまでlockファイルを更新するために書いたに過ぎないから。直接プロジェクトの依存として使っているわけではないし、resolutionsに書いたままにしておくと、あとになってなんでresolutionsに書かれて固定されているのかわからなくなる。

しかもresolutionsに書いてあると永遠に固定されるので、今回の例だと今後 "lodash": "4.17.19" 以降のバージョンが出ても永遠に "4.17.19" を利用しようとするからよくない。なのでpackage.jsonから記述は消してしまって、lockファイルだけを更新しておく。


以上、自分がDependabotになるにはどうすればよいかという話。長かったね。

Dependabotがやってくれないとか待ってられないときは自分たちがDependabotと同じことをするしかないので覚えておいて損はない気がする。

こういうのなかなか知見が書かれにくいし表現しにくいので書いた。