世界を動かす技術を、日本語で。

Pnpmはサプライチェーン攻撃を防ぐための新しい設定を導入しました

概要

  • pnpm に新しいセキュリティ設定と依存関係検索機能を追加
  • minimumReleaseAge で新規リリース依存パッケージのインストール遅延が可能
  • finder functions により依存関係の高度な絞り込みが実現
  • 複数の バグ修正 と利便性向上のパッチ適用
  • 実用例や設定方法も明記

新機能:依存関係アップデートの遅延設定

  • minimumReleaseAge 設定で、公開直後のパッケージインストールを一定時間遅延
  • 例: minimumReleaseAge: 1440 で、24時間以内に公開されたパッケージはインストール不可
  • 攻撃による悪意あるバージョン流通リスクの低減
  • minimumReleaseAgeExclude で特定の依存関係のみ遅延対象外設定が可能
    • 例:webpackを常に最新でインストール
  • 関連Issue: #9921

依存関係フィルタリングの高度化:finder functions

  • finder functions サポートで、依存パッケージを名前以外の条件でも検索可能
  • .pnpmfile.cjs にfinder関数を定義し、--find-by=<関数名>で呼び出し
    • 例:peerDependenciesに react@17 を持つパッケージの検索
  • 検索結果には依存関係ツリー上の正確な位置を表示
  • finder関数から文字列を返すことで、追加情報も出力可能
    • 例:ライセンス情報の表示
  • 関連PR: #9946

パッチ・バグ修正

  • Node.js 24 実行時の非推奨警告の修正(#9529)
  • nodeVersion が厳密なsemverでない場合、エラーをスロー(#9934)
  • pnpm publish で.tar.gzファイルの公開が可能に(#9927)
  • Ctrl-C によるプロセス中断時、pnpm runが非ゼロ終了コードを返すよう修正(#9626)

Hackerたちの意見

みんなが侵害されたパッケージの最新バージョンをインストールするのに3日待つなら、事件を検出するのに3日以上かかるよね。

たくさんの人がnpmを使うから、彼らが炭鉱のカナリアになるんだろうね :) もっと真面目に言うと、自動スキャナーはすでに悪意のあるパッケージを見つけるのが得意みたい。npm自体が自動対策を導入してないのが不思議だよ。

いや、アプリセキュリティの会社は常にnpmをスキャンして、マルウェアの更新パッケージをチェックしてるから。多くの攻撃はそのおかげで捕まってるよ。例えば、debugとchalkのサプライチェーン攻撃はこうやって見つかった: https://www.aikido.dev/blog/npm-debug-and-chalk-packages-com...

  1. チェックや監査はまだ行われるだろうし(もし行われていればだけど) 2) 所有者が侵害されていることに気づくリアルなチャンス 3) そのコモンズが完全に悲劇になる前に早めに採用すること。

最近の3つの大きなインシデントがどうやって発覚したか考えてみて。個々のユーザーがパッケージをインストールするのではなく、セキュリティ会社が新しいアップロードに対して自動スキャンを実行して監査のためにフラグを立てたからだよ。このモデルではうまく機能すると思うし、何か新しく出たものをインストールする必要がない場合は、多くのケースで安価だよ。

それに、みんなが妥協されたパッケージの最新バージョンをインストールするのに3日待つつもりなら、野良での妥協に対する修正が広まるのに3日以上かかることになるよね。結局、どっちもどっちだよ。

chalk+debug+error-exのメンテナーは、家に帰って「成功裏に公開されました」っていうnpmからのメールがたくさん来てるのを見て、数時間後に気づくんじゃないかな。

それがnpmの役割だから、最初にインストールするんだよね。使い捨てのやつら。

名前に単位を含めるか、値の一部として単位を選択する必要があったね。ごめん、ちょっと気になるポイントなんだ。

それか、ISO8601の標準表記を使えばいいんじゃない?(例えば「P1D」で1日)

ISO8601の期間を使うべきだよ、例えばPT3Mみたいに。

新しい設定は古いものと一貫してるから、私的にはそっちの方が重要だと思うよ: https://pnpm.io/settings#modulescachemaxage

質問なんだけど、これに関して話してる人たちが「3日」や「7日」をタイムアウトとして使うって言ってるのを見たことがあるんだけど、プロダクションで使うには短すぎる気がする。C++開発者としては、リリースから最初の6ヶ月は依存関係を使うのに躊躇しちゃうな、重大なCVEがない限り(まあ、うちは基本的にネットワークなしのクライアントサイドアプリを作ってるから、セキュリティはそこまで重要じゃなくて、安定性の方が大事なんだけど)。JSエコシステムって、パッケージを更新するのに1ヶ月や2ヶ月待てないほど速く動いてるの?

そうだけど、これはJSだけの問題じゃないよ。PHP(composer)でも同じ。通常、古いメジャーやマイナーパッケージは更新されないから、最新のものだけが更新される。例えば、4.1.47(更新なし)、4.2.1(更新あり)。だから、4.1に問題があったら4.2に「アップグレード」しなきゃいけない。完璧なセマンティックバージョニングなら問題ないはずだけど、現実はそうじゃないからね。

NPMパッケージはセマンティックバージョニングに従ってるから、マイナーバージョンは自動更新しても大丈夫だと思う。(パッケージのメンテナが考えるマイナーが、あなたにとってはマイナーじゃないこともあるけど、理想の世界の話としておこう)人々が毎月メジャーバージョンを更新してるとは思わないし、実際には6ヶ月に1回か、年に1回くらいだと思う。自動更新でマイナーバージョンをCI/CDパイプラインで更新すれば、バグ修正がマイナーバージョンに含まれるからもっと安全になると思ってる人もいるかもしれないけど、実際にはそうじゃなくて、攻撃者がそれを利用してマルウェアを広めてるのが現実だよ。

JSエコシステムって本当にそんなに早く動いてるの?パッケージを更新するのに1、2ヶ月待てないの?2ヶ月で、典型的なJSフレームワークはガートナーのハイプサイクルを経て、メンテナンスされなくなり、アーカイブされたgitリポジトリと似た名前のウイルス感染したフォークがたくさん出てくるんだ。

JSエコシステムって本当にそんなに早く動いてるの?パッケージを更新するのに1、2ヶ月待てないの?実際、文脈やコードが使われている場所によるね。他の人も指摘しているように、ほとんどのJSパッケージはセマンティックバージョニングを使ってる。パッチリリース(3つの数字の最後の部分)は、外部に公開されているコードにはできるだけ早く適用したい。これにはCVEを修正するホットフィックスが含まれてるからね。メジャーとマイナーリリースは、どんな依存関係を使っているか、そしてそれがどれだけ安定しているかによる。問題は、JavaScriptエコシステムに特有のものではないよ。大きなJavaプロジェクト(特に多くのSpring関連の依存関係があるもの)でも、たくさんの動きがある。JavaScriptエコシステムが非常に不安定だというトロープが完全に間違っているわけではないけど、今回は文脈が大きな違いだと思う。> とはいえ、私たちは基本的にネットワークなしでクライアントサイドアプリケーションを作るから、セキュリティはそれほど重要じゃない。安定性の方がずっと大事だね。大抵のJavaScriptは、悪意のあるアクターがたくさんいる環境では何らかの形でネットワークに接続されているからね。

C++の依存関係のバグの発生可能性は、JSのそれよりもずっと大きいと思う。例えば、新しいノードモジュールを取り込んでも、アプリがセグメンテーションフォルトを起こすことはないからね。

npmの監査を有効にしているのは一般的で、これによってCI/CDが古いパッケージにセキュリティの脆弱性が報告された場合、新しいバージョンに更新することを強制されることになる。パッケージにバグを見つけて、バグレポートやPRを提出した後、修正されたらすぐに新しいバージョンを取り込むこともあった。JavaScript/npm/GitHubのエコシステムは動きが早いよ。

依存関係をアップグレードするのに6ヶ月待つのはクレイジーだね。他の言語や会社ではそんなことはないと思う。(優先順位の問題かもしれないけど、何かのルールによるものではない)JVMエコシステムでは、DependabotやRenovateがリリースから数時間以内に依存関係のアップグレード用のPRを自動で作成するのが一般的だよ。手動だとかなり不規則で、会社によるね。

推移的依存関係が主な問題だね。例えば、バージョン1.0.0のパッケージP1がバージョン^1.0.0のD1に依存しているとする。この「^」は範囲クエリを示していて、セマンティックバージョニングの詳細には触れないけど、D1を自動的にマイナーパッチや非破壊的な機能追加のために更新するのに役立つんだ。プロジェクト内では、P1が1.0.0に固定されているから問題なさそうに見える。でも、D1を使っているP2をインストールすると、新しいパッチバージョンのD1(1.0.1)がリリースされて、パッケージマネージャーが自動的に1.0.1にアップグレードしちゃう。これはP1とP2の作者が指定した^1.0.0に合致するから。これが驚きにつながることもある。JSのパッケージマネージャーはインストール中の変更を防ぐためにロックファイルを使うけど、追加や手動のバージョンアップグレードのためにロックファイルは変わるし、バージョン範囲が合えば新しいマイナー依存関係に解決される。これはバグ修正やセキュリティアップデートには望ましいことが多いけど、こういう攻撃の隙を作ることにもなる。質問に答えると、はい、JSエコシステムは動きが早くて、パッケージマネージャーが小さなライブラリを簡単に作れるようにしている。その結果、推移的依存関係としてたくさんの「小さな」ライブラリが存在することになる。左パッドのような簡単なケースなら、自分のコードでこれらのライブラリを書き換えることはできるけど、たくさんの小さな推移的依存関係を持つウェブサーバーやビルドツールを再構築することはできないよ。例えば、chalkライブラリは多くのCLIツールでカラー出力を表示するために使われている。

「遅延依存関係の更新」は、JavaScriptの世界における供給側攻撃への対応だけど、技術全般に対する私のアプローチをうまく表してる。大手テック企業は、ほとんどの業界と同じように、ほとんどの人が金を払う前にプライバシーやデータで支払うことに気づいてる。結局、今は注意の通貨の時代だからね。でも、テクノロジーを使った生活を送るのに、カナリアになる必要はないよ。プライバシーやデータで支払うソフトウェアの多くには、同じかそれ以上の品質を持つ無料または安価なオープンソースの代替品がある。自分の消費の仕方を「まあ、私を尊重してくれるバージョンができるまで待てるし」と向けると、いろんな面で生活がもっと楽しくなる。これを絶対的なレベルに持っていくつもりはないけど、今は高級なLLMにお金を払ってる。でも、近い将来、私のホムラボで今日の品質を無料で手に入れられる日を楽しみにしてる。

uvはこのことに早くから賛同してたから、評価されるべきだと思う。最初は自分たちのテスト用に安定したフィクスチャを作る隠れた方法として追加したけど、今ではかなり人気のあるフラグになってる。例えば、これを使うと14日以上古いパッケージだけをインストールすることができる:uv sync --exclude-newer $(date -u -v-14d '+%Y-%m-%dT%H:%M:%SZ') こういうのがもっと広まってるのを見るのは嬉しいね。

いいね、でも設定ファイルの方がサプライチェーン攻撃、特に開発者を狙ったものに対して守るための実装としてはずっと良いと思う。毎回インストールする時にフラグを渡すことに頼りたくないからね。ただ、npm installの代わりにpnpm installを使うリスクがあるのは確かだね。設定してないプロジェクトでも使えるようにフラグとしてもあればいいなと思うけど、それも追加できるのかな。

私はちょっと naive かもしれないけど、なぜどのパッケージマネージャー(npm、pnpm、bun、yarnなど)が、パッケージがpackage.jsonでどの権限にアクセスしたいかを定義する必要があるような権限システムを推進してないの?Denoみたいに依存関係にスコープを絞ったり、モバイルアプリがマニフェストでやってるみたいに。これを導入するのには時間がかかるのは分かってるけど、新しい依存関係をインストールする時にパラメータとして実装できると思う。例えば、npm i ping --allow-netみたいに。chalkみたいなライブラリにI/Oやプロセス、ネットワークへのアクセスを与えたくないな。

言語の側面、もしくは少なくともランタイムの作業が必要になりそうだね。例えば、あるパッケージ内のコードがネットワークにアクセスするのを止める方法ってあるのかな?インストールスクリプトの周りでこれができるかもしれないけど、全てにディスク書き込みが必要になるだろうし(でも、場所を制御できるかもしれないね)。

npmがこういう機能を追加しない理由を理解している人いる?

2022年にこの機能についてのNPMのRFCがあったけど(サプライチェーン攻撃に特化してはいなかったけど)、主な反応はここにある他のコメントと似たようなものでした。「待つことがセキュリティを高めるわけじゃないし、もしそんな習慣が一般的になったら、脆弱性の発見がその時間の後に遅れるだけだよ」 https://github.com/npm/rfcs/issues/646#issuecomment-12824971...

Yarnも似たような機能を追加したみたいだよ。 https://github.com/yarnpkg/berry/pull/6901

しかも、特定のバージョンを除外することもサポートしてる!Yarnが再び注目されてるみたいだね。

NPMや他のパッケージマネージャーの問題に対する正しい解決策は、以下に基づく信頼のウェブ監査システムだと思う。 - 実際に公開されたパッケージのソースコードをレビューすること - パッケージのバージョンとその前のバージョンの間で信頼できる差分を簡単に確認できるツール - パッケージマネージャーのCLIに、信頼できるソースからの十分な数の手動レビューがあるパッケージだけをインストールするためのサポートを組み込むこと(+ ネガティブレビューが少ないこと)。これらのチェックをバイパスするには手動レビューが必要。各パッケージのユーザーは十分にいるから、インフラが整えばこのシステムはユーザーにとってそれほど負担にはならないはず。

自分はnpmユーザーなんだけど、ソフトウェアサプライチェーン攻撃に対する反応は、脆弱性の緩和に絶対必要な場合を除いて更新を止めることかな。それか、パッケージごとに選んでパフォーマンスや機能のアップグレードを受け入れる感じ。もちろん、そのアプローチだと、悪意のあるパッケージのリリースと更新のタイミングが重なったときに攻撃を受けるリスクがあるけど、極端にアップグレードを避けることで大体は安全だと思ってる。目標を達成するために、このアプローチはどうかな? - package.jsonのバージョンをすべて固定する(バージョンの前に~や^をつけない) - ローカルとCIサーバーの両方でパッケージを定期的にインストールするのはnpm ciを使う - npm install --save-exact/--save-devはpackage.jsonにパッケージを追加するときだけ使い、その後にnpm ciを実行する - GitHub DependabotやCodeQLのようなツールに頼って、セキュリティの理由で依存関係を更新すべきときにチームに知らせてもらい、手動で必要なバージョンの依存関係だけをnpm install lodash@4.17.21 --save-exactのように更新する。追記:これについて考えると、package-lock.jsonを削除してnpm installで再生成するのを禁止し、npm updateの使用も禁止してpackage-lock.jsonが安定するようにしなきゃね。

それが良いのは、ある依存関係が互換性を壊して、持ってるバージョンのサポートをやめる日が来るまでだよね。そうなったら、何年も放置してたから依存関係の解決に何日もかかることになる。普通は、段階的でタイムリーなアップグレードがそういう摩擦を減らすんだよ。