はじめに
npmはJavaScriptエコシステムでスタンダードとなっているパッケージマネージャーですが、近年のサプライチェーン攻撃の増加に伴い、pnpmへの移行を検討しています。
今回は公式ドキュメントを参考にしつつ、npmからpnpmへ移行する手順と注意点をまとめます。
npmサプライチェーン攻撃については前回の記事にまとめています。
pnpmとは
公式サイト: Fast, disk space efficient package manager | pnpm
npmと比べた場合のpnpmのメリットについても公式でページが用意されています。
翻訳がわかりづらいので、要点をまとめると以下の通りです。
1. ディスク容量の節約
npmの場合、同じ依存関係を使うプロジェクトが複数ある場合に、各プロジェクトごとに依存関係のコピーが保存されます。
pnpmの場合は依存関係は一か所に保存され、異なるバージョンの場合は更新されたファイルだけが追加されます。全体がコピーされるわけではありません。 つまり、パッケージのファイルは一か所に保存され、インストール時はそこからハードリンクされるため、追加のディスク領域を消費しない仕組みです。
結果として、プロジェクト数、依存関係数が増えたとしてもディスク容量を節約でき、結果としてインストールも非常に高速になります。
2. インストール速度の向上
npmの場合は「解決 -> 取得 -> node_modulesへの書き込み」の順番でインストールされます。
pnpmの場合は
- 依存関係の解決
- ディレクトリ構造の計算
- 依存関係をリンク
の順番でインストールされ、依存関係の取得とリンクが並列で行われるため、全体として高速になると説明されています。
3. フラットではないnode_modulesを作る
npmやYarn Classicの場合は、node_modulesがフラットな構造になりますが、この方法ではプロジェクトが直接依存していない依存関係に対してもアクセスできるようになります。
pnpmはデフォルトでシンボリックリンクを利用して、プロジェクトの直接の依存関係のみをモジュールディレクトリ直下に追加します。 なお、ツールがシンボリックリンクと相性が悪い場合はnodeLinker設定をhoistedにすることで、npmやYarn Classicと同様のフラットな構造に変更できます。
npmサプライチェーン攻撃に対するpnpmの優位性
こちらも公式サイトに記載がありますが、翻訳がまだ存在しないようなので要点をまとめます。
概要
npmパッケージがマルウェア混入などによって侵害された状態で公開されることがあり、検出、削除までには数時間から数日の時間がかかることがあります。検出までの時間には常に一定の時間がかかり、いわゆるゼロデイ攻撃を防ぐことはできないとされていますが、pnpmにはリスクを最小限に抑えるための設定や仕組みがあります。
危険なインストール前後のスクリプトのブロック
これまで報告されているサプライチェーン攻撃の手法として、preinstall/postinstallスクリプトを利用して悪意のあるコードを実行する方法があります。pnpm v10では、依存関係に含まれる postinstall の自動実行をデフォルトで無効化しています。もちろん、意図的に有効化する dangerouslyAllowAllBuilds オプションも存在しますが、基本的には allowBuilds オプションで個別に許可する形式を推奨しています。
これにより、過去にビルド不要だった依存が、侵害されたバージョンの公開で突然悪意あるスクリプトを実行することを回避できます。
仕組み上、postinstall を元から持っている信頼済みのパッケージの更新を防ぐことはできませんが、一定のリスク低減には寄与します。
異常な推移依存の防止
blockExoticSubdeps=true を設定することで、推移依存がgitリポジトリや直接tarball URLなどの「exotic sources」を使うのを防止します。
推移依存が信頼できるソースからのみ解決されることを保証します。
公式サイトに書かれているのはこれだけですが、推移依存について説明しておきます。
まず「推移的依存関係」とは
あなたのプロジェクトが直接入れている依存(直接依存)だけでなく、その依存がさらに別の依存を持っている場合があります。
あなたのプロジェクト → A(直接依存)
A → B(推移依存 / 間接依存)
B → C(さらに推移依存)
このBやCが「推移的依存関係」です。
つまり、自分が直接指定していない依存が勝手にGitや直接URLからコードを取ってくるのを禁止する仕組みがpnpmには用意されている、ということですね。
依存更新を遅らせる
多くの場合、セキュリティ攻撃は数時間から数日程度で検出され、対策のパッチがリリースされます。つまり、依存関係の更新を最新版の更新からある程度時間を空けてから行うことで、攻撃に巻き込まれるリスクを下げることができます。
pnpmでは、minimumReleaseAge を設定することで、「公開後、pnpmがそのバージョンをインストール可能になるまでの最小経過時間(分)」を指定することができます。
trustPolicyによる「信頼レベル低下」の拒否
pnpmでは、trustPolicy を利用でき、no-downgrade に設定することで過去のリリースより信頼レベルが下がったパッケージがインストールされることを防ぎます。
例外的に許可したい場合は trustPolicyExclude を利用できます。
trustPolicyについても説明しておきます。
公式サイトでは下記のページに解説があります。
npmのサプライチェーン攻撃からニュースルームを守る方法 | pnpm
npm側にも解説があります。
npm パッケージの信頼できる公開 | npm ドキュメント
pnpmでは、パッケージの信頼レベルを次のような段階で評価しています。
- Trusted Publisher
例: GitHub ActionsなどのCI/CDからOIDCを使う「trusted publishing」でpublishされ、かつnpmのprovenanceが付く場合
- Provenance(来歴証明)
CI/CDからの署名付きattestation(来歴情報)が付与されている場合
- No Trust Evidence(証拠なし)
ユーザー名/パスワードまたはトークン認証などで公開され、provenanceなどの信頼根拠がない状態
サプライチェーン攻撃の特徴として、攻撃者がnpmメンテナのアカウントを乗っ取って悪意のあるパッケージを公開するケースがあります。この場合、メンテナの認証情報は盗めても、CI/CDで利用するOIDC側の権限までは取れていないことが多く、その場合にはtrusted publisherやprovenanceが付与されていない状態で公開されることが多いため、信頼レベルが下がります。
つまり、no-downgrade を設定することで、「いつもは強い手段(trusted publishing / provenance)で出しているはずのパッケージが、急に弱い手段で出てきた」場合にインストールを止めることができます。
まとめ
npmからpnpmへの移行によって、高速化するだけでなく、多くの攻撃に対するリスクを低減することができそうです。
また、pnpm公式サイトの最後にlockfileの利用による予期せぬ更新を避けるべきという記述もあり、こちらはnpmでも同様に実現できる手段です。多くのプロジェクトでは行われていますが、古めのプロジェクトなど、漏れがないかを年末の棚卸しとして確認しておきましょう。
