フロントエンドエキスパートチームの小林(@koba04)です。
先日、npmから脆弱性についての発表がありました。 調べていく中でいくつか思うところがあったので解説も兼ねて書いていきたいと思います。
The npm Blog — Binary Planting with the npm CLI
npmの利用者としてやるべきことは、
- npmのバージョンを6.13.4以上にあげる
- yarnのバージョンを1.21.1以上にあげる
です。 npmのバージョンが6.13.4になったNodeもv8, v10, v12, v13系でそれぞれリリースされたので、そちらを利用することも可能です (yarnのバージョンは別途あげる必要があります)。
npmによる発表では、今回発表された脆弱性は2件あるため、それぞれ個別に考えます。
binに任意のパスを指定出来る件
npmパッケージはpackage.json
のbin
フィールドに、インストールすることで使えるようになるコマンドを指定出来ます。
例えば、eslint
の場合は下記のように指定されているため、インストールするとeslint
コマンドがnpm-scriptsやnpx経由で利用可能になります。
パッケージをグローバルにインストールしない場合、このコマンドはnode_modules/.bin
に配置されます。
npm-scriptsの実行時はここにPATHが通っているため、npm-scriptsではeslint
コマンドとして実行できます。
今回は脆弱性の話なので詳しく解説しませんが、bin
に興味持った人は下記のドキュメントを参照してください。
今回の脆弱性はここのコマンド名の部分に、../../cd
などと指定したら…?という話です。
現状では特にコマンド名に対するチェックが行われていなかったため、インストールするだけで任意のファイルを置き換えることが可能であり、これが脆弱性であると判定されました。
npmでは、この辺りのcommitで修正されています(内部で利用している別パッケージの修正)
yarnでは、この辺りのcommitで修正されています
この脆弱性は、孫依存のような開発者自身が明示的に指定せずにインストールされる依存パッケージからでも悪用可能なため、対応すべきです。
グローバルにインストールしたパッケージが別のコマンドを上書きできる件
npmはプロジェクトローカルにパッケージをインストールするだけでなく--global(-g
)フラグを使うことでグローバルにパッケージをインストールできます。
yarnの場合は、global
コマンドを利用します。
グローバルにパッケージをインストールした場合、パッケージが提供するコマンドのシンボリックリンクが/usr/local/bin
以下に作成されます(作成される場所は実行環境により異なります)。
/usr/local/bin
といえば、他にもHomebrewなどでインストールしたパッケージのコマンドも配置される場所です。
この脆弱性は、グローバルにインストールされたパッケージが、bin
に指定しているコマンドによってすでにある別のコマンドを上書きできるというものです。
例えばjq
をHomebrewか何かでインストールしている状態で、悪意のあるnpmパッケージをグローバルにインストールした場合、そのbin
フィールドに"jq" : "evil.js"
のように書かれていると、jqコマンドが上書きされてしまうということです。
グローバルにインストールすること自体がそんなに多くないのと、よくわからないnpmパッケージをグローバルにインストールすることはほとんどないと思うので、1つめのものに比べると深刻度は低いかなと思います。
npmでは、この辺りのcommitで修正しています。
対応方法としては、すでにコマンドが存在する場合、シンボリックリンク先が今からインストールしようとしているものと同じかどうか確認しています。
yarnでは、これに対する修正はまだ行われていません。 なので、下記のissueでどうするのか聞いてみました。
上記のIssueによると、yarnでは今回の件を下記の3つに分けて考えています。
- Binary planting
- Out-of-tree execution
- Binary overlap
Binary plantingは1つ目に紹介した脆弱性で、前述した通りyarnは1.21.1で修正済みです。
Out-of-tree executionは、binフィールドのコマンド名の部分ではなく、ファイルパスを指定する側に/home/foo/bar
のように指定することで任意のポイントに対するシンボリックリンクを作成できるというものです。
{ "bin": { "foo": "/home/foo/bar" } }
これについては、Binary plantingの対応と同時に対応されています。 ただしこちらに関しては、脆弱性ではなくbugfixという扱いにしています。
Binary overlapは2つ目に紹介した件であり、yarnでは対応されていないものです。 対応しない理由としては、上記のIssueでは下記のように説明されています。
- 過去にBinary overlapを意図的にやっていたパッケージがあり、その挙動に対してBreaking Changeとなるため
- これらのパッケージは、ユーザーが意図してインストールしたパッケージであるため
そのため、特にグローバルなパッケージをインストールする場合には、注意深く行う必要があります。
上記のIssueによると、yarnはv2でbin
スクリプトはNodeを通じてのみ実行可能にすることを計画しているようです。
これにより、Node上でサンドボックス環境を構築してその上で実行させることが可能にできる可能性があります。
yarnはNodeがこのようなセキュリティポリシーを実装することを期待しています。
postinstallとinstall
今回の脆弱性についてはここまでですが、前述したIssueではpostinstall
の危険性についても言及されています。
npmでは、publishやpackなど、任意のタイミングで呼ばれるscriptをnpm-scriptsとして登録出来る仕組みがあります。 どのようなscriptを登録出来るのかは下記のページより確認可能です。
その中に、install
とpostinstall
があります。
これらは利用者がパッケージをインストールする際に実行されるスクリプトです。
つまり、ただnpm install
するだけでインストールしたパッケージに対して任意のスクリプト実行を許可していることになります。
そのため、結果的に悪意のあるパッケージが依存関係に含まれてしまった場合には、npm install
するだけで悪意のあるスクリプトを実行されてしまいます。
つまり、今回のような脆弱性を利用せずとも悪意のあるパッケージはうまく依存関係に含まれることに成功すれば任意のスクリプトをユーザー環境で実行できます。 …。
そのための対策として、npm-scriptsの実行をしないための--ignore-scripts
というオプションがあります。
これを利用することでインストール時に任意のスクリプトの実行を拒否することが可能です。
ただ、これを使うためにはなぜinstall
やpostinstall
が使われているのかを理解する必要があります。
現状、下記の2点の用途で利用されていることが多いと思います。
- ユーザー環境に応じた処理
- C, C++などのコードを含んだネイティブモジュールのビルド
- 環境に応じたバイナリのダウンロード
- Fundingのお願い
1のケースで言えば、fsevents
やpuppeteer
などがよく目にケースではないでしょうか。
ただ、fsevents
に関しては下記のPRでinstall
スクリプトを実行しないようになっているので、最新版だとインストールスクリプトは実行されません。
puppeteerでは下記のスクリプトでchromiumのバイナリをダウンロードします。
2.のケースは、最近npm install
やnpm ci
の際に大量のfundingのお願いのメッセージを目にすることがあるのではないでしょうか?
これは、多くの場合postinstall
を使ってconsole.log
でメッセージを出力することで実現されています。
ではこれらをなくすことが出来るのかということですが、まず2のケースについては言えば、npm v6.13からサポートされているfunding
のフィールドを使うことで、npm fund
コマンドで出力することができます。
The npm Blog — Updates to Community, Docs & more...
npm install
でパッケージをインストールした時にnpm fund
に対応したパッケージが依存にあると、下記のような形で出力されます。
: added 15 packages from 17 contributors and audited 15 packages in 1.574s 2 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
npm fund
を実行すると下記のような形で出力されます。
% npm fund test@1.0.0 └─┬ fetch-mock@8.2.0-beta.2 ├── type: charity ├── url: https://www.justgiving.com/refugee-support-europe └─┬ core-js@3.6.0 ├── type: opencollective └── url: https://opencollective.com/core-js
今回の脆弱性により、多くのユーザーはnpm 6.13.4以上を利用するようになるため、npm fund
コマンドが利用可能になります。
そのため、postinstall
ではなくnpm fund
を使うための準備が整ったとも言えます。
yarnは現在fund
コマンドをサポートしていませんが、前述したIssueによるとv2には実装してバックポートすることが検討されているようです。
1のケースについては、パフォーマンス目的でC, C++で書いている場合には、WebAssemblyで置き換えることが可能かもしれません。 ただ、NodeがサポートしていないネイティブのAPIを使いたい場合には、現時点ではまだ使い続ける必要がありそうです。 将来的にはこれについてもWebAssemblyで解決出来る可能性はあるかもしれませんが。
そもそもの現状として、どんなスクリプトが自分のプロジェクトのnpm install
時に実行されているのか把握している人は少ないのではないでしょうか?
インストール済みのパッケージでinstall
またはpostinstall
のスクリプトがあるものを出力するためのnpm packageを作ってみたので興味ある人は自身のプロジェクトで実行してみてください。
% npx install-scripts
こちらは社内のプロジェクトで実行した結果です。
この中で動作に必要なscriptはfsevents
とpuppeteer
だけでした。他は全てfunding関連です。
core-js scripts: postinstall: node scripts/postinstall || echo "ignore" paths: node_modules/@babel/polyfill/node_modules/core-js/package.json node_modules/@storybook/addon-actions/node_modules/core-js/package.json node_modules/@storybook/addon-knobs/node_modules/core-js/package.json node_modules/@storybook/addon-links/node_modules/core-js/package.json node_modules/@storybook/addon-viewport/node_modules/core-js/package.json node_modules/@storybook/addons/node_modules/core-js/package.json node_modules/@storybook/api/node_modules/core-js/package.json node_modules/@storybook/channel-postmessage/node_modules/core-js/package.json node_modules/@storybook/channels/node_modules/core-js/package.json node_modules/@storybook/client-api/node_modules/core-js/package.json node_modules/@storybook/client-logger/node_modules/core-js/package.json node_modules/@storybook/components/node_modules/core-js/package.json node_modules/@storybook/core/node_modules/core-js/package.json node_modules/@storybook/core-events/node_modules/core-js/package.json node_modules/@storybook/node-logger/node_modules/core-js/package.json node_modules/@storybook/react/node_modules/core-js/package.json node_modules/@storybook/router/node_modules/core-js/package.json node_modules/@storybook/theming/node_modules/core-js/package.json node_modules/@storybook/ui/node_modules/core-js/package.json node_modules/fetch-mock/node_modules/core-js/package.json node_modules/lazy-universal-dotenv/node_modules/core-js/package.json node_modules/simplebar/node_modules/core-js/package.json node_modules/wait-on/node_modules/core-js/package.json core-js-pure scripts: postinstall: node scripts/postinstall || echo "ignore" paths: node_modules/core-js-pure/package.json fetch-mock scripts: postinstall: node scripts/support-fetch-mock.js paths: node_modules/fetch-mock/package.json fsevents scripts: install: node install paths: node_modules/fsevents/package.json puppeteer scripts: install: node install.js paths: node_modules/puppeteer/package.json styled-components scripts: postinstall: node ./scripts/postinstall.js || exit 0 paths: node_modules/styled-components/package.json
fetch-mock
は最新版ではすでにfund
を使うようになっていますcore-js
についてはこちらで議論されていますが、今のところ削除する予定はなさそうですstyled-components
については過去に議論されたIssueがなかったので確認してみました
下記のように組み合わせることで必要なスクリプトのみを実行することも可能です。
% npm ci --ignore-scripts
% npx install-scripts
yarnはpostinstallについて、既存のエコシステムを壊してしまうのでv2で全て無効にすることは難しいですが、v3では考えているようです。 ただし、現時点でも手動でホワイトリスト管理によって許可されたscriptのみ実行可能にすることは可能だとしています。
実際にpostinstall
、install
を調べてみた結果としては、そこまで多くないなという感想だったので--ignore-scripts
を使った運用するのも可能であるように感じました。
そのためにも、今度はpackageをホワイトリストで指定して実行できるnpm packageを作ってみようかなと思いました。
ただこれ自体はnpm自体が対応すべき問題であると感じるので、Feature Requestを送りました。
以前には下記のevent-stream
に対する問題がありましたが、現状ではインストールするパッケージを全て把握することは現実的ではありません。
The npm Blog — Details about the event-stream incident
今回の脆弱性を通じて、npmの現状や将来の方向性などを考える機会になればいいなと思ってこの記事を書きました。
サイボウズでは、脆弱性があった時にcommit log読んで原因を探求したくなるエンジニアを募集しています!