npm ベストプラクティス ガイド
(このページは npm Best Practices Guide の参考訳です。)
バージョン 1.1
このドキュメントは、npmのパッケージ マネージャーを使用する際のセキュリティ サプライ チェーンのベストプラクティスを説明する包括的なドキュメントを目指しています。これは、OSSFのBest Practices for Open Source Developersワーキング グループの活動の1つです。この文書が提供するのは、
- サプライ チェーンのコンテキストにおけるnpmのセキュリティ機能の概要
- 明示的な推奨事項
- これらの推奨事項を達成するための詳細または公式ドキュメントへのリンク。このドキュメントは、公式のnpmドキュメントを補完することを目的としており、代替となるものではありません。
目次
CIの構成
CI 構成では最小権限の原則に従ってください。
GitHub Actions経由でCIを実行する場合、非特権環境は、GitHubシークレットにアクセスできず書き込み権限を持たないワークフローにしてください( permissions: read-all、permissions:、contents: none、contents: read など)。権限の詳細については、公式ドキュメントを参照してください。
OpenSSFスコアカード アクションをインストールして、 プロジェクトの非読み取り権限にフラグを立てることができます。
依存関係の管理
取り入れる
依存関係を使用するための最初のステップは、そのオリジン、信頼性、セキュリティ体制を調査することです。Envoyプロキシのようなプロジェクトには、使用される前に満たさなければならない依存関係の条件が明確に文書化されています。
推奨事項:
- 攻撃者が公式に見えるパッケージ名を作成し、ユーザーをだまして不正なパッケージ(1、2、3)をインストールさせる、タイポスクワッティング攻撃に注意してください。npmレジストリはタイポスクワッティングを検出するためにスキャンを実行しますが、完璧なシステムはありませんので、常に警戒してください。パッケージのGitHubリポジトリを特定し、貢献者やスターの数などを通じてその信頼性を評価します。
- さらに、以前は大文字が許可されていましたが、これも注意が必要な攻撃の特徴です。例: JSONStream、jsonstream。
- 注意: 非ASCII文字はnpmパッケージ名でサポートされなくなりました。そのため、開発者はパブリック レジストリからの同形異義語の攻撃について心配する必要はありません。攻撃者は、ASCII文字に似たレンダリングを行う非ASCII文字を使用してパッケージに名前を付けます。このプロパティはレジストリに依存することに注意してください。使用するレジストリでこのポリシーを確認する必要があります。
- 対象となるGitHubプロジェクトを特定したら、そのドキュメントに従って対応するパッケージ名を確認します。
OpenSSFセキュリティ スコアカードを使用して、依存関係の現在のセキュリティ体制を確認します。 - deps.devを使用して、間接的な依存関係のセキュリティについて学習します。コンポーネント分析を使用して、関節的な依存関係をリストすることができます 。
- npm-auditを使用して、プロジェクトの依存関係に存在する脆弱性を確認します。
警告:npmレジストリに組織に対する検証は存在せず、「先着順」です。紛争プロセスを使用し、組織の所有者に対して不正に取得した名称に対する異議申し立てを行うことができます。
宣言
npmでは、package.jsonにプロジェクト(名前、コンポーネント、依存関係など)を記述します。依存関係は、パッケージ名、URL、リポジトリなどによって定義できます。どのタグ、ブランチ、バージョン、またはコミットが許可されるかなど、依存関係ごとに追加の制約を定義できます。
注意:マニフェスト ファイルには間接的な依存関係がリストされません。プロジェクトの直接の依存関係のみが記述されています。
プロジェクトの種類
このドキュメントの残りの部分では、次の 3 種類のプロジェクトについて説明します。
- ライブラリ:これらは、npmレジストリで公開され、API呼び出しの形式で他のプロジェクトによって使用されるプロジェクトです。(マニフェスト ファイルには通常 main、exports、browser、module、および/または types のエントリが含まれます)。
- スタンドアロンCLI:これらは、npmレジストリで公開され、ローカルにインストールされたプログラムの形式でエンドユーザーによって使用されるプロジェクトであり、常に単体でnpx、またはグローバル インストールを介して実行されます。例としては、clipboard-cliがあります。(マニフェスト ファイルには bin エントリが含まれています)。
- アプリケーション プロジェクト:WebサイトやWebアプリケーションなど、チームが共同で開発および展開するプロジェクトです。例としては、企業のユーザー向けSaaS用のReact Webアプリが挙げられます。(通常、マニフェスト ファイルには “private”: true が含まれます)。
再現可能なインストール
再現可能なインストールは、パッケージがインストールされるたびに依存関係が使用され、同一のコピーとなり正確であることが保証されます。これには次のようなさまざまな利点があります。
- インストールされている依存関係が、プル リクエストを通じて宣言およびレビューされたものであることを確認します。
- 依存関係の1つに脆弱性が見つかった場合に、インフラストラクチャの侵害の可能性を迅速に特定するのに役立ちます。これは、リポジトリが危険にさらされている場合に、コミット範囲を即座に判断できるためです。
- 悪意のある依存関係などの特定の脅威を軽減します。そうしない場合、新しく公開された(侵害された)バージョンの依存関係をCI/CDシステムまたは開発者マシンにインストールして実行し、攻撃者に即座にコードが実行される可能性があります。
- RAM の破損などによるパッケージの破損をインストール前に検出します。
- パブリック レジストリからパッケージをプロキシするレジストリの構成ミスによって引き起こされるパッケージの変更可能性を軽減します。注意:バージョンは原則として不変であり、レジストリによって不変性が強制されます。
- GitHubのセキュリティ アラートなどの自動ツールの精度を向上させます。
- デフォルトのブランチで更新を受け入れる前に、メンテナーが更新をテストできるようにします (例: renovatebotのstabilityDaysを使用) 。
再現可能なインストールを確実に実現するには、依存関係のあるファイルをリポジトリに取り込むか、とハッシュによる固定の2つの方法があります。
ロックファイルの利用
暗号化ハッシュを使用してハッシュの固定を実装するため、ロックファイルを使用します。ハッシュの固定は、レジストリを信頼せずに、各依存関係に対して予想されるハッシュをパッケージ マネージャーに伝える方法です。各インストール中に、パッケージ マネージャーは各依存関係のハッシュが同じであることを確認します。依存関係の中で悪意のある変更があれば、すべて検出され、拒否されます。
npmには、ハッシュの固定を実現するための2つのオプションが用意されています。
package-lock.json
package -lock.jsonには、すべての 依存関係 (直接および間接) とそのコンテンツの暗号化ハッシュが含まれています。
{
"name": "A",
"version": "0.1.0",
...metadata fields...
"dependencies": {
"B": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/B/-/B-0.0.1.tgz",
"integrity": "sha512-DeAdb33F+"
"dependencies": {
"C": {
"version": "git://github.com/org/C.git#5c380ae319fc4efe9e7f2d9c78b0faa588fd99b4"
}
}
}
}
}
このpackage-lock.jsonファイルはインストールのスナップショットであり、後で同じインストールを再現できるようになります。そのため、ロック ファイルは、パッケージをインストールするさまざまなコマンドを介して生成または更新されます。例えば、npm installです。一部の依存関係が失われているか、ハッシュによって固定されていない場合 (integrityフィールドが存在しないなど)、インストールはロック ファイルにパッチを適用してインストールを反映します。
ロック ファイルはレジストリにアップロードできません。つまり、npm installを通じてパッケージをローカルにインストールする利用者には、リポジトリのオーナーがテスト中に使用したものとは異なる依存関係のバージョンが表示される可能性があります。package-lock.jsonの利用は、低レベル プログラミング言語のダイナミックリンクに似ています。ローダーは、システムで利用可能なライブラリを使用して、ロード時に依存関係を解決します。このロック ファイルを使用すると、使用する依存関係のバージョンを決定するタスクがパッケージの利用者に任されます。
npm-shrinkwrap.json
npm-shrinkwrap.jsonは 、npmでサポートされるもう1つのロック ファイルです。主な違いは、このロック ファイルがパッケージとともにレジストリにアップロードされる可能性があることです。これにより、利用者はリポジトリ所有者が意図したものと同じ依存関係のハッシュを取得できるようになります。npm-shrinkwrap.jsonでは、パッケージの作成者が利用者に代わって依存関係を更新する責任を負います。これは低レベル プログラミング言語の静的リンクに似ており、パッケージ化時にすべてが宣言されます。
npm-shrinkwrap.jsonを生成するには、既存のpackage-lock.jsonが存在し、コマンドnpm shrinkwrapを実行する必要があります。
ロックファイルとコマンド
特定のnpmコマンドはロックファイルを読み取り専用として扱いますが、他のコマンドは読み取り専用として扱いません。
次のコマンドは、ロック ファイルを読み取り専用として扱います。
- npm ci、プロジェクトとその依存関係をインストールするために使用されます。
- npm install-ci-test、プロジェクトをインストールし、テストを実行するために使用されます。
次のコマンドはロック ファイルを読み取り専用として扱わず、固定されていない依存関係をフェッチ/インストールし、ロック ファイルを更新する可能性があります。
- npm install, npm i, npm install -g
- npm update
- npm install-test
- npm pkg setand npm pkg delete
- npm exec, npx
- npm set-script
推奨事項:
- 開発者は、すべてのプロジェクトのマニフェスト ファイルを宣言してコミットする必要があります。マニフェスト ファイルを作成するには、公式のjson ファイルの作成ドキュメントを使用してください。
- マニフェスト ファイルに依存関係を追加するには、ローカルで npm install –save <dep-name> を実行し、更新されたマニフェストをリポジトリにコミットします。
- レジストリからスタンドアロン CLI パッケージを実行する必要がある場合は、ビルド時にCI またはその他の自動化された環境にインストールする前に、そのパッケージがpackage.jsonファイルを介してプロジェクトで定義された依存関係の一部であることを確認してください。
- 開発者は、すべてのプロジェクトのロックファイルを宣言してコミットする必要があります。その理由は、このロックファイルは、特権環境(プロジェクト開発者のマシン、CI、本番環境やその他環境(シークレット、PIIなどの機密データへのアクセス、リポジトリへの書き込み/プッシュ等)に対して、デフォルトで再現可能なインストールができることという利点を提供するためです。
テストをローカルで実行する場合、意図的に依存関係を追加/削除する場合を除き、開発者はロックファイルを読み取り専用として扱うコマンドを使用する必要があります (「ロックファイルとコマンド」を参照)。
以下では、プロジェクトの種類ごとに受け入れ可能なロックファイルについて説明します。 - プロジェクトがライブラリの場合:
- npm-shrinkwrap.jsonは公開すべきではありません。その理由は、バージョン解決はパッケージの利用者に任せるべきであるということです。サポートする最小バージョンから最新バージョンまでのすべてのバージョンを許可します。たとえば、^m.n.oメジャー バージョンを固定、~m.n.oマイナー バージョンを固定 です。重大な脆弱性のあるバージョンはできる限り避けてください。範囲を定義するには、semver 計算ツールにアクセスしてください。
- ロックファイルpackage-lock.jsonは、CIで実行されるテスト (例: npm install –no-package-lockなど)では無視する必要があります。その理由は、CIテストでは、ユーザーの利用の前に広範囲の依存関係バージョンを実行し問題の発見/修正を行うため、テストでは最新バージョンのパッケージを取得する必要があるということです。
- ローカルでは、依存関係を意図的に追加/削除する場合を除き、開発者はロックファイルを読み取り専用として扱う npm コマンドのみを実行する必要があります (「ロックファイルとコマンド」を参照)。
- CIの設定は最小特権の原則に従ってください。ロックファイルは無視されることがあるため、これは特に重要です。
- プロジェクトがスタンドアロンCLIの場合:
- 開発者はnpm-shrinkwrap.jsonを公開することができます。npm-shrinkwrap.jsonを定義すると、あなたにすべての依存関係を迅速かつ一貫して更新する責任が生じることに注意してください。ユーザーはそれらを編集したり重複を排除したりすることはできません。CLI が他のプロジェクトで使用され、そのプロジェクトのpackage.jsonまたはlockfileで定義されることが予想される場合は、コンシューマーの依存関係の解決を妨げるため、 npm-shrinkwrap.json は使用しないでください。プロジェクトがライブラリの場合の推奨事項に従ってください。
- CIでは、ロックファイルを読み取り専用として扱うnpmコマンドのみを実行します(「ロックファイルとコマンド」を参照)。
- ローカルでは、依存関係を意図的に追加/削除する場合を除き、開発者はロックファイルを読み取り専用として扱うnpmコマンドのみを実行する必要があります(「ロックファイルとコマンド」を参照)。
- CIの設定は最小特権の原則に従ってください。
- プロジェクトがアプリケーションの場合:
- 開発者は、ロックファイルを定義してリポジトリにコミットする必要があります。
- CIでは、ロックファイルを読み取り専用として扱うnpmコマンドのみを実行します(「ロックファイルとコマンド」を参照)。
- ローカルでは、依存関係を意図的に追加/削除する場合を除き、開発者はロックファイルを読み取り専用として扱うnpmコマンドのみを実行する必要があります(「ロックファイルとコマンド」を参照)。
依存関係のベンダー化
依存関係のベンダー化とは、すべての依存関係(直接的および推移的)のローカル コピーをリポジトリ内に保持することを意味します。ベンダーは「再現可能なインストール」の問題を解決できますが、安全でない慣行を助長する可能性もあります。これには、依存関係をもつコードを監査する機能が不十分であること、依存関係を最新の状態に保つことが困難であることなどが含まれます。また、ベンダー化により、リポジトリのサイズ、使いやすさ、開発者のエクスペリエンスの問題など、セキュリティ以外の問題も発生します。これらの理由から、このドキュメントの範囲外の問題に対処するためのツールやソリューションがなければ、ベンダー化は推奨されません。
メンテナンス
依存関係は定期的に更新することが重要です。特に新しい脆弱性が公開され、パッチが適用された場合にはさらに重要になります。依存関係の管理に役立つツールは使いやすく、セキュリティ チェックを実装することができるかもしれません。
推奨事項:
- 依存関係を管理するには、dependabotやrenovatebotなどのツールを使用します。これらのツールがマージ リクエストを送信し、それを確認してデフォルトのブランチにマージすることができます。
- 依存関係に関する既存の脆弱性について最新の情報を入手してください。
- 上記のツールをインストールしている場合は、セキュリティ アラートを有効にします。「dependabot config」 および「renovatebot config」を参照してください。Renovatebotはconfig-as-codeに従っているため、外部ユーザーがこの機能が有効になっているか、簡単に確認できることに注意してください。
- 専用ツールを使用したい場合は、GitHubワークフローなどでnpm-auditを定期的に実行してください。
- 依存関係を削除するには、npm prune を定期的に実行してマージ リクエストを送信します。上記のツールはまだこの機能をサポートしておらず、この機能に対するGitHubアクションは認識されていません。
脆弱性の開示
脆弱性の開示は、大きく2つの部分に分かれます。研究者は脆弱性を発見してソフトウェアのメンテナーに報告し、ソフトウェアのメンテナーはソフトウェアのユーザーに既知の脆弱性を通知します。OpenSSFは、脆弱性の開示に関する一連の一般的な推奨事項を管理しており、メンテナーは詳細について問い合わせてください。
研究者からメンテナーへの開示
ソフトウェアのメンテナーは、脆弱性を非公開で公開する方法について、簡単かつ明確にする必要があります。GitHubでは、研究者が発見したセキュリティの脆弱性を非公開で開示するために使用できる連絡先情報を含むsecurity.md ファイルを作成することを推奨しています。メンテナーは、善意のセキュリティ研究者が一般向けにすべての脆弱性情報を開示せざるを得なくならないように、何らかのプライベートな通信方法が可能であることを保証する必要があります。
メンテナーからユーザーへの開示
ほとんどのプロジェクトでは、それらが利用されていればどこかで脆弱性が発見されることが多いです。そして、それらの脆弱性は明確かつ簡潔な方法でユーザーに開示されることが重要となります。CVE、GHSA、またはその他のインデックス番号を使用して脆弱性を公開すると、自動化されたシステムが関連する脆弱性を検出、取り込み、ユーザーに報告できるようになります。これらの自動化されたシステムが適切に機能するためには、ソース アドバイザリで特定のnpmパッケージ名、バージョン、問題を解決するコードの変更を可能な限り詳細に記載する必要があります。
リリース
アカウント
npmレジストリで公開するには、ユーザー アカウントを作成する必要があります。
推奨事項:
- 開発者は自分のアカウントで二要素認証を有効にする必要があります。
署名と検証
npmパッケージはすべて、デフォルトのnpmレジストリが所有するECDSAキーによって署名されています。
署名は、npm CLI v8.15.0 以降でnpm audit signaturesコマンドを使用して検証することが可能です。
警告:npm はユーザーレベルの署名をサポートしていません。
公開
パッケージを公開すると、他の人がインストールおよびダウンロードできるように、そのパッケージがレジストリにアップロードされます。
推奨事項:
- CIシステムを使用して公開する場合は、デフォルトのnpmレジストリへ認証、公開するためオートメーション トークンを使用してください。
- 次のコマンドを使用してパッケージをリリースします。
npm cinpm publish
- 公開したパッケージの利用者は、タイポスクワッティングの被害に遭う可能性があります。この問題を軽減するには、他のレジストリ上に組織を作成して管理してください。
注意:
- デフォルトのnpmレジストリは認証用にデフォルトのGitHubトークンをサポートしていないため、ユーザーはそれをGitHubシークレットとして保存する必要があります。
- npmパッケージを公開するための公式のGitHubアクションはありません。
- CIDRスコープのトークンは、保護を強化するためにIPの範囲によってスコープが設定されていますが、これはnpm CLIでのみ作成できます。
プライベート パッケージ
依存関係かく乱攻撃、別名、置換攻撃は、プロジェクトが内部レジストリのプライベート パッケージに依存している場合に発生します。攻撃者が同じパッケージ名をパブリック レジストリに登録する可能性があります。デフォルトでは、npmはパブリック レジストリから依存関係を取得します。Alex Birsanのブログで最近の攻撃の詳細を参照してください。npmが正しい依存関係を取得できるようにこの動作を変更するには、スコープを利用できます。
スコープ
「スコープ」は、パッケージ名の先頭に付けられる@が付いた名前です。たとえば、@myorg/fooはスコープ付きパッケージです。スコープ付きパッケージは、package.jsonおよびJavaScriptコード内の他のパッケージと同様に使用されます。
{
"name": "@myorg/foo",
"version": "1.2.3",
"description": "just a scoped package name example",
"dependencies": {
"@myorg/bar": "2.x"
}
}
// es modules style
import foo from "@myorg/foo";
// commonjs style
const foo = require("@myorg/foo");
パブリックnpmレジストリ上のスコープが付与されたパッケージは、それに関連するユーザーまたは組織によってのみ公開でき、そのスコープ内のパッケージはプライベートにすることができます。また、スコープ名を特定のレジストリにリンクすることもできます。
推奨事項:
- パブリックnpmレジストリ上に、「myorg」という名前の無料の組織を作成します。その時点で、他の誰もパブリック レジストリのスコープ内で何も公開できなくなり 、構成が間違っている場合、信頼できないコンテンツを静かに取得するのではなく、404エラーにより@myorgビルドが失敗します。これは、攻撃者がパブリック レジストリ内の組織名を乗っ取り、同じ問題が発生する可能性を防ぐため、非常に重要です。
- ローカルでは、loginコマンドを使用して、@myorgスコープ内のパッケージに対するすべての要求がhttps://registry.myorg.localに対して行われるようにします。スコープにバインドされていないその他のリクエストは、デフォルトのレジストリに送られます。
npm login --scope=myorg --registry=https://registry.myorg.local
これにより以下のように、~/.npmrcファイルにログインの情報が保存されます@myorg:registry = https://registry.myorg.local///registry.myorg.local:_authToken = xyzabc123-arbitrary-token-value
- 認証情報を含むこの~/.npmrcファイルをリポジトリにコミットしないでください。
- 自動化された環境では、シークレットを自動的にプロビジョニングします。GitHubがワークフローで使用しているものと同様のソリューションに従います。レジストリが一時的な認証情報をサポートしている場合は、長期のシークレット/トークンを使用する代わりに、それを使用してください。
- プロジェクトのルートに次のような行を含む.npmrcファイルを作成します。
@myorg:registry = https://registry.myorg.local/
このコマンドを実行すると、現在構成されているレジストリをいつでも確認できます。npm config get registry
これを行うと、npmはそのプロジェクトで作業するときにスコープを内部レジストリに関連付けます。
このトピックの詳細については、npmの置換攻撃に関するブログ投稿を参照してください。
プライベート レジストリ構成
内部プライベート レジストリを使用する場合
- スコープをサポートするプライベート レジストリ ソリューションのみを使用してください
- プライベートパッケージを不変にします。
- レジストリがアップストリームのパブリック レジストリから同じ名前のマニフェストを「マージ」するように構成されていないことを確認してください。これは、解決の衝突を回避するために有効になることがありますが、まさに「解決の衝突の回避」が名前ハイジャックのエクスプロイトの仕組みであるため、これは最善とは言えません。可能であれば、この機能すら持たないプライベート レジストリ実装を使用してください。
- パッケージが内部プロキシ レジストリに公開された後は、そのパッケージが削除された場合に、暗黙的にパブリック レジストリにフォールバックしないことが非常に重要です。プロキシがパブリック レジストリからこのパッケージ名を提供し始めると、攻撃者がその名前を乗っ取り、残されたシステムにアクセスできる状況に戻ります。
- ビルドの失敗を無視しないでください。プロジェクトを上記のように設定すると、npmが信頼できないコンテンツを取得するのではなく、404エラーが表示される可能性があります。これらのエラーを無視しないでください。ビルドが失敗した場合にできるだけ大きな音でクラッシュするようにシステムを構成し、すぐに修正します。
このトピックの詳細については、npmの置換攻撃に関するブログ投稿を参照してください。
翻訳協力:松本央