PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

社内システムの Node.js バージョン更新と依存パッケージの脆弱性対応を行いました

こんにちは、SaaS事業部の鐘川です。

しばらく使用されずに放置されていた社内システムが最近また必要になったので、そのシステムの一部であるReactを使用したフロントエンドアプリのNode.jsバージョンアップ対応を行いました。今回はその対応中に起こったエラーの解消方法やバージョンアップ後のパッケージの脆弱性対応などの手順をまとめていきます。

本記事の内容はmacでの操作を想定しています。

環境

Node.jsのパッケージ管理にはyarnを、バージョン管理にはvoltaを使用しています。

  • OS: mac
  • Node.js: v10.21.0 → v18.12.1
  • yarn: v1.22.19
  • volta: v1.0.8

Node.js 18にバージョンアップ

現在のNode.jsのバージョンを確認

バージョンアップ前のNode.jsのバージョンはv10.21.0でした。

$ node -v
v10.21.0

Node.js 18のインストール

voltaを使用してインストール

voltaは簡単に使用できるNode.jsのバージョン管理ツールです。
voltaを使用することでプロジェクト毎にNode.jsのバージョンを自動的に切り替えることが可能になります。

volta.sh

voltaを導入する場合は公式ドキュメントを参考にしてください。

docs.volta.sh

voltaを使用して当時最新LTSであったv18.12.1をインストールしました。

$ volta install node@18.12.1

Node.jsのバージョンを固定

voltaでNode.jsのバージョンを固定します。バージョンを固定しておくことで、プロジェクト内でのバージョン管理が楽になります。
Node.jsのバージョンを先ほどインストールした v18.12.1に固定します。

$ volta pin node@18.12.1
success: pinned node@18.12.1 in package.json

コマンドを叩くとpackage.jsonも更新されます。

package.json

"volta": {
  "node": "18.12.1"
}

再度Node.jsのバージョンを確認すると、v18.12.1に更新されているのが確認できました。

$ node -v
v18.12.1

OpenSSLの互換性エラー対策

Node.js 17から、OpenSSLのバージョンが以前までのv1.1.0からv3.0.0に更新されました。 これが原因でOpenSSLのバージョンと互換性が取れず、エラーが発生することがあります。 当アプリケーションでも以下のようなエラーが発生しました。

Error: error:0308010C:digital envelope routines::unsupported

このエラーを解消するためには、以下のように環境変数を指定します。
--openssl-legacy-providerオプションを使用することでOpenSSL3.0レガシー・プロバイダーが有効になります。

$ export NODE_OPTIONS=--openssl-legacy-provider

nodejs.org

nodejs.org

Node.js 18へのバージョンアップ後、アプリケーションを起動しようとすると、

$ yarn start

エラーが発生しました。

Error: Node Sass does not yet support your current environment:

原因はCSSメタ言語であるSassのコンパイルを行うパッケージであるnode-sassのバージョンが古く、Node.js 18に対応していないことでした。
node-sassのバージョンアップをしようとも思いましたが、調べたところ公式ではnode-sassは2020年10月から非推奨となっており、現在はdart-sassが推奨されています。
この機会にnode-sassからdart-sassへの移行を行いました。

www.npmjs.com

node-sassをdart-sassに移行

dart-sassのインストール

node-sassをアンインストールしてdart-sassをインストールします。
ややこしいですが、dart-sassのパッケージ名はsassです。

$ yarn remove node-sass
$ yarn add sass

v1.58.3がインストールされました。

$ yarn list --depth=0 | grep sass  
├─ postcss-sass@0.4.4
├─ sass-loader@6.0.7
├─ sass@1.58.3

node_modulesを再インストール

Node.jsのバージョンアップが原因で、パッケージに不具合が発生する可能性や依存関係が変更される恐れがあるため、一度node_modulesを再インストールします。 依存関係を新しく更新したいのでyarn.lockは削除。古いままの依存関係がキャッシュされている可能性があるので、yarnのキャッシュも削除しておきます。
package.jsonがあればyarn.lockは一度削除してもyarn install実行完了時に再生成されますが、問題があった場合にプロジェクトを復元できるようにバックアップをとっておくことをお勧めします。

$ yarn cache clean 
$ rm -rf node_modules yarn.lock
$ yarn install

再度アプリケーションを起動しようとすると、

$ yarn start

別のエラーが発生。

Module build failed: Error: Cannot find module 'node-sass'

原因はSassをCSSに変換するパッケージであるsass-loaderのバージョンが古く、dart-sassに対応していなかったためでした。 sass-loaderのバージョンが古い場合はnode-sassが必須であり、そのnode-sassを削除したため、エラーが発生しています。
v7.1.0以降ではdart-sassに対応し、node-sassまたはdart-sassのどちらかが必須となっています。

www.npmjs.com

sass-loaderのバージョンアップ

sass-loaderのバージョンを確認すると、v6.0.7でした。

$ yarn list --depth=0 | grep sass-loader
├─ sass-loader@6.0.7

これをv7.3.1にアップグレードします。
さらに新しいバージョンもありますが、v7.3.1より新しいバージョンを指定するとパッケージの依存関係が壊れてアプリケーションが正常に動作しないことが確認できたため、依存関係が壊れない範囲の最新バージョンであるv7.3.1を指定しています。

$ yarn upgrade sass-loader@7.3.1

その後yarn startでアプリケーションの起動が成功しました!

パッケージの脆弱性解消

Node.js 18でのアプリケーション起動は成功しましたが、併せて行いたいのがパッケージの脆弱性解消です。 脆弱性を解消しておくことで、より安全にアプリケーションを運用することができます。

脆弱性のあるパッケージ調査

yarnを使用している場合はyarn auditで脆弱性のあるパッケージを調査できます。
今回は--groups dependenciesと指定することで、アプリケーション実行時に使用するパッケージに絞り込んで脆弱性対応を行います。

$ yarn audit --groups dependencies

...
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ critical      │ Prototype Pollution in minimist                              │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ minimist                                                     │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in    │ >=0.2.4                                                      │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ multer                                                       │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ multer > mkdirp > minimist                                   │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://www.npmjs.com/advisories/1091172                     │
└───────────────┴──────────────────────────────────────────────────────────────┘
184 vulnerabilities found - Packages audited: 749
Severity: 11 Low | 48 Moderate | 99 High | 26 Critical

コマンドを叩くと、脆弱性のあるパッケージ情報一覧と最後に内訳が表示されます。 脆弱性レベルはLow Moderate High Criticalの4段階に分類され、脆弱性が見つかった場合にはパッケージ更新を検討します。 今回は749個のパッケージ中、184件の脆弱性が見つかる結果となりました。

脆弱性解消

以下の方法で脆弱性解消を行っていきます。

  1. yarn upgradeでパッケージを更新して脆弱性解消
  2. 依存関係を調査して大元にあるパッケージを更新
  3. 依存パッケージのバージョンを指定

1. yarn upgradeでパッケージを更新して脆弱性解消

yarn upgradeを実行すると、package.jsonに記述されたバージョンの範囲でパッケージを更新します。 yarn upgrade --latestと実行した場合はpackage.jsonを無視してパッケージを最新バージョンに更新するため脆弱性解消は見込めますが、更新後のパッケージに破壊的変更が加わっていた場合はアプリケーションが正常に動作しなくなる恐れがあります。

$ yarn upgrade

再度脆弱性調査を行うと、

$ yarn audit --groups dependencies  

...
36 vulnerabilities found - Packages audited: 643
Severity: 2 Low | 17 Moderate | 14 High | 3 Critical

脆弱性件数が184件から36件にまで減少しました。 総パッケージ数が749個から643個に減っていることから、不要なパッケージが削除されていることも確認できます。

2. 依存関係を調査して大元にあるパッケージを更新

yarn upgradeを実行しても脆弱性が解消できなかった36個のパッケージについてはpackage.jsonで指定されたバージョンの範囲が古いことや、パッケージの依存関係が深く、バージョン更新が行えなかったことが考えられます。これらのパッケージについては依存関係を調査し、その大元にあるパッケージを更新して脆弱性が解消されるか確認します。 例として今回脆弱性が発見されたminimistの更新を行います。

$ yarn audit --groups dependencies

...
┌───────────────┬──────────────────────────────────────────────────────────────┐
│ critical      │ Prototype Pollution in minimist                              │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ minimist                                                     │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in    │ >=0.2.4                                                      │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ pm2                                                          │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ pm2 > mkdirp > minimist                                      │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://www.npmjs.com/advisories/1091172                     │
└───────────────┴──────────────────────────────────────────────────────────────┘
...
36 vulnerabilities found - Packages audited: 643
Severity: 2 Low | 17 Moderate | 14 High | 3 Critical

パッケージの依存関係をyarn auditの結果から調査します。 Dependency ofの行を見るとminimistの依存関係の大元にあるのはpm2であることが確認できます。
既にyarn upgradeは実行しているので、pm2package.jsonに指定された範囲内での最新バージョンのはずです。package.jsonyarn.lockを確認してみます。

package.json

"dependencies": {
  ...
  "pm2": "^2.9.1",
  ...
}

package.jsonには"pm2": "^2.9.1"と記述されています。
^(キャレット)が指定されている場合は、「一番左の0以外の数字を変更しない最新バージョン」を意味するので、今回のpm2の場合はv2.9.1 <= version < 3.0.0の範囲の最新となります。^以外にも~(チルダ)や不等号なども指定できます。

github.com

yarn.lock

pm2@^2.9.1:
  version "2.10.4"
  resolved "http://registry.yarnpkg.com/pm2/-/pm2-2.10.4.tgz#dd292fd26aed882f6e9f7b9652191387d2debe6a"
  integrity sha512-AuAA6DoF/R3L9zSuYtKzaEd6UFvhCKqfW49dgLe0Q4SQtYmQMmXmyEAp5tr1iduJrqGRwpb5ytVm2rWZ56/4Vg==
  dependencies:
    async "^2.5"
    blessed "^0.1.81"
    chalk "^1.1"
    chokidar "^2"
    cli-table-redemption "^1.0.0"
    commander "2.13.0"
    cron "^1.3"
    debug "^3.0"
    eventemitter2 "1.0.5"
    fclone "1.0.11"
    mkdirp "0.5.1"
...

yarn.lockを確認すると、実際にインストールされているpm2のバージョンはv2.10.4であることが確認できました。公式を確認するとpackage.jsonで指定したv2.9.1 <= version < 3.0.0の範囲の最新はv2.10.4だったので、依存関係が深くてバージョン更新に失敗したわけではなく、package.jsonで指定したバージョン範囲の最新に更新はされているが、その範囲指定が古かったことが脆弱性解消できなかった原因だと分かります。この場合はpackage.jsonの範囲指定を超えて更新を行う必要があります。

$ yarn upgrade pm2 --latest

--latestを指定して、package.jsonを無視して最新バージョンへ更新します。 このとき、パッケージの公式ドキュメントを見て、バージョンアップ後に破壊的変更がされていないかの確認は必須です。破壊的変更があった場合は、yarn upgrade pm2 @バージョン数のようにバージョンを指定して破壊的変更がないバージョンへ更新、もしくは別パッケージへの移行を検討します。 今回のpm2は最新バージョンへの更新で問題なく、v5.2.2に更新されました。

再度脆弱性調査を行うと、minimistの脆弱性が解消されているのが確認できました。
脆弱性件数は36件から31件に減少しています。

$ yarn audit --groups dependencies  

...
31 vulnerabilities found - Packages audited: 593
Severity: 2 Low | 15 Moderate | 12 High | 2 Critical

3. 依存パッケージのバージョンを指定

依存関係の大元にあるパッケージを更新しても脆弱性が解消しない場合や、破壊的変更などがあり、大元のパッケージを更新できない場合はpackage.jsonresolutionsを指定することで、依存パッケージのバージョンを固定することが可能です。
ただし強制的にバージョンを指定するため、依存関係が壊れないか、アプリケーションへの影響はないかの確認を慎重に行う必要や、今後依存関係が更新された場合にはresolutionsでバージョンを指定したパッケージについては手動で依存関係を更新する必要があります。

classic.yarnpkg.com

先ほどのminimistの脆弱性をこの方法で解消してみます。yarn audit実行結果のPatched inの行を見ると、どのバージョンを指定すれば脆弱性が解消できるか確認できます。>=0.2.4とあるので、v0.2.4以上のバージョンを指定します。依存関係はパッケージ名を/で区切って指定できます。

package.json

...
"dependencies": {
  ...
},
"resolutions": {
  "pm2/mkdirp/minimist": "^0.2.4"
}

その後yarn installyarn auditを実行してminimistの脆弱性が解消されていることが確認できました。

他のパッケージについてもこれらの方法で脆弱性対応を行いました。

終わりに

今回はNode.jsのバージョンアップ、パッケージの脆弱性対応を行いました。
これからNode.jsのバージョンアップを検討している方のお役に立てれば幸いです。