Skip to main content

npm依存関係の5つの側面

2016年6月16日

0 分で読めます

npmの依存関係が増え続けていること、そしてそれが生産性と開発速度を高める一方で、脆弱性や不安定さの原因にもなることについて、よく話題になります。では、npmの依存関係とは具体的に何でしょうか?

Snykの製品は依存関係のセキュリティ確保に重点を置いているため、まず依存関係とはそもそも何を指すのかを定義する必要がありました。この記事では、依存関係をさまざまな側面から取り上げ、わかりやすい分類体系を作ろうとする中で得た知見を共有し、依存関係をどう分類できるのかを理解する手助けをします。

基本的な定義:依存しているコード

最も簡潔に定義すると、依存関係とは、アプリケーションが依存するコードのパッケージです。このコードがなければ、アプリケーションは正しく動作せず、ビルドさえできないこともあります。

そのため、整理の仕方にかかわらず、すべての依存関係は何らかの形でアプリケーションの機能、信頼性、セキュリティに影響します。それを念頭に置いて、依存関係をさまざまな切り口から見ていきましょう。

側面1:開発用と本番用

最初に取り上げる、最も明確な依存関係の種類は、開発用と本番用です。package.jsonファイルでは、本番環境で使用する依存関係はdependenciesとして、開発時にのみ使う依存関係はdevDependenciesとして明示的に記載します。

npm installコマンドは、デフォルトで現在のアプリの開発用と本番用の依存関係を両方インストールします。一方、npmからダウンロードしたパッケージについては、本番用の依存関係のみをインストールします。

たとえば、npmのutilパッケージでは、package.jsonファイルの依存関係セクションを次のように記載しています。

{
  "name": "util",
...
  "dependencies": {
    "inherits": "2.0.1"
  },
...
  "devDependencies": {
    "zuul": "~1.0.9"
  },
...
}

npm install utilを実行すると、本番用の依存関係であるinheritsのみがインストールされます。一方、リポジトリをクローンし、クローンしたフォルダでnode-utilとnpm installを実行すると、inheritsとzuulの両方がインストールされます。

前述のとおり、この区別はアプリケーションによって明示的に行われ、その考え方は非常にシンプルです。アプリケーションの実行に必要な依存関係は、本番用にします。テストやビルドにのみ必要なら、開発用です。脆弱性を調べる際、開発用の依存関係はそれほど重要ではないため、Snykではデフォルトで本番用の依存関係のみをテストします(--devフラグで変更できます)。

peerDependenciesとoptionalDependenciesも本番用の依存関係ですが、インストールされるタイミングや方法には少し違いがあります。論理上の依存関係とディスク上の依存関係について説明する際に、これらを取り上げます。

WP-Calypsoのコード構成を示すグラフ。プロジェクトコードが15.67%、依存関係コードが61.98%、開発依存関係コードが22.35%

上の画像は、bitHoundによる開発用と本番用の依存関係の比率の例です。

側面2:直接依存と間接依存

依存関係の中には、package.jsonファイルに明示的に記述する直接依存(プライマリ依存とも呼ばれます)があります。しかし、大半は間接依存(セカンダリ依存とも呼ばれます)で、直接依存(または別の間接依存)が機能を果たすために取り込むものです。ほとんどのアプリケーションでは、依存関係全体の大部分を間接依存が占めます。たとえば、left-padを直接使うアプリケーションはごくわずかですが、babelやnodeなど、よく知られた一部のパッケージは直接使っています。そのためleft-padは非常に多くのアプリケーションにとって間接依存となり、公開停止が大きな影響をもたらしました。

アプリケーションから遠く離れ、依存関係ツリーの奥深くにネストされたパッケージは、存在を知らなかったり、すっかり忘れてしまったりしがちです。しかし、間接的な依存関係も依存関係であることに変わりはありません。left-padが削除されたことで、利用していることさえ知らなかったアプリケーションも含め、多くのアプリケーションが動かなくなりました。深い階層にある依存関係のライセンスを遵守しなければ、法的問題が生じる可能性があります。また、遠く離れた間接依存の脆弱性から、攻撃者に侵入されることもあります。

側面3:パッケージとバージョン

requestパッケージのバージョン2.11.3を使っているとします。この場合、何に依存しているのでしょうか?パッケージであるrequestでしょうか。それとも特定のバージョンであるrequest@2.11.3でしょうか?

答えは、その両方です。明らかに、request@2.11.3に依存しています。このバージョンは、コードに取り込んで使用している変更不可能なプログラムです。このパッケージに欠陥があれば、たとえばこのリモートメモリ露出の脆弱性のような問題がコードに影響します。また、このパッケージが公開された際に適用されたMITライセンスを遵守する責任もあります。

一方で、プロジェクトとしてのrequestパッケージにも依存しています。semverのバージョン範囲を指定して利用しているなら、作者がマイナーバージョンの修正で互換性を損なう変更をリリースしないことを前提としています。セキュリティの面では、npmやGitHubの認証情報を漏えいさせないこと、事前にセキュリティ上の問題をテストすること、報告された脆弱性を速やかに修正することを期待しています。長期的には、プロジェクトと作者が適切にメンテナンスし、バグ修正や機能追加を適時行うことにも依存します。使用するパッケージのバージョンを固定するにはshrinkwrapを使うか、依存関係をバンドルすることで、このリスクを軽減できます。ただし、どちらの場合も新機能やバグ修正を受け取れなくなります。

パッケージとバージョンはどちらも依存対象ですが、依存の仕方は大きく異なるため、それぞれに別の名前を付けるとよいでしょう。残念ながら、パッケージとバージョンの組み合わせを表す明確な用語はなく、パッケージという言葉はどちらの意味にも使われています。

Snykでは、依存関係という場合、通常はrequest@2.11.3のようなパッケージとバージョンの組み合わせを指します。パッケージ自体を指す場合は、明確に依存先パッケージと呼びます。ただし、ここでの分類はやや複雑です。私たちはこのガイドラインに沿うよう努めていますが、文脈から意味を判断してもらう前提で、単に「パッケージ」と呼ぶこともあります…

「request」、範囲「2.x」、一致する2.xのセマンティックバージョンの一覧を表示したパッケージバージョンセレクター。

requestのようなパッケージには多くのバージョンがあります。それぞれの品質と、適切に管理するプロジェクトに依存しています。

側面4:論理上とディスク上

ここまでの説明はすべて、依存関係ツリーを概念上どう捉えるかという論理上の依存関係に関するものでした。しかし、実際にディスクへダウンロードすると、ツリーの構造は大きく変わることがあります。その一因は、インストールのされ方がやや状況に左右されるpeer依存関係やoptional依存関係ですが、npm3の重複排除も大きく影響します。

inflightパッケージを見てみましょう。次に示すのは、その論理上の依存関係ツリーです。

inflight@1.0.5
├─┬ once@1.3.3
│ └── wrappy@1.0.2
└── wrappy@1.0.2

wrappy@1.0.2は、直接依存としても、once@1.3.3を介した間接依存としても使われています。inflightのリポジトリをクローンし、npm install --productionとnpm lsを実行すると、次のようになります。

inflight@1.0.5
├── once@1.3.3
└── wrappy@1.0.2

ご覧のとおり、wrappy@1.0.2は一度しか表示されません。これはnpm3の重複排除によるもので、重複したパッケージを検出し、ディスク上に別のコピーを作らないようにします。npm2でもデフォルトで基本的な重複排除が行われます。また、npm dedupeを実行して明示的に行うこともできます。

ここではシンプルな例を使っていますが、semverのバージョン範囲やnpmのインストールを繰り返すケースでは、複雑になり予測しにくくなることがあります。snyk-resolveを使うと、論理上とディスク上の依存関係を確認できます。Remyがこのブログ記事で紹介しています。

この側面で重要なのは、論理上の依存関係とディスク上の依存関係が異なる場合があり、ディスク上の依存関係はインストールのロジックや順序によって変わるという点です。個々の直接依存の論理だけでなく、アプリケーション全体に実際に何がインストールされたかを確認してください。

側面5:依存関係の経路と一意な依存関係

依存関係を定義したところで、最後の側面として、依存関係の数え方を見ていきましょう。次の論理上の依存関係ツリーを考えてみてください。

app@1.2.3
├─┬ A@1.0.0
│ └── B@1.0.0
├─┬ C@1.0.0
│ └── B@2.0.0
└── B@2.0.0

このツリーを見ると、アプリにはA、B、Cの3つの依存先パッケージがあると言えます。また、重複排除によって冗長なものが取り除かれるため、バージョンを含めて、ディスク上の依存関係はA@1.0.0、B@1.0.0、B@2.0.0、C@1.0.0の4つだと言えます。では、論理上の依存関係はいくつでしょうか?パッケージとバージョンの組み合わせごとに数えて4つでしょうか。それとも、依存関係ツリーのノードごとに数えて5つでしょうか?

この2つを区別するには、このアプリには一意な依存関係が4つあり、依存関係の経路が5つあると言うとよいでしょう。たとえばB@2.0.0に既知の脆弱性がある場合、Snykでは既知の脆弱性は1件、脆弱な経路は2つと表現します。

まとめ

まとめると、依存関係には複数の側面があり、それぞれに適した用途があります。依存関係について話すときは、会話がスムーズに進むよう、できる限り同じ分類体系を使うようにしましょう。

要点をまとめると、5つの側面は次のとおりです。

  1. 開発用と本番用:アプリのビルドとテストには開発用依存関係が、実行には本番用依存関係が必要です。

  2. 直接依存と間接依存:アプリが明示的に必要とするのは直接依存だけですが、品質、法務、セキュリティのレビューでは、より数の多い間接依存も対象にする必要があります。

  3. パッケージとバージョン:デプロイしたアプリに影響するのは依存関係の特定のバージョンですが、プロジェクトが正常に機能し続けるには、依存先パッケージに頼っています。

  4. 論理上とディスク上:アプリの論理上の依存関係ツリーは、ディスクへのインストール時に大きく変わることがあります。実際にインストールされたバージョンを必ず確認しましょう。

  5. 経路と一意な依存関係:依存関係を数えるときは、タスクや問題の規模を正確に見積もるために、一意な依存関係の数と依存関係の経路数を分けて考えましょう。

注:npmとyarnのパッケージマニフェストや、アプリケーションとライブラリにおけるロックファイルの仕組みを詳しく解説した記事もご覧ください。

用語がわかったところで、Snykを使ってアプリケーションをテストし、利用している脆弱な本番用依存関係と依存関係の経路の数を確認できます。また、Vulnerability DBで依存先パッケージを検索し、過去にセキュリティ上の問題があったかどうかを調べることもできます。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。

カテゴリー: