はじめに
こんにちは、クラウドサインでフロントエンドエンジニアをしています篠田 (@tttttt_621_s) です。
普段のソフトウェア開発において、OSS の活用は欠かせません。クラウドサインも多くの OSS に支えられています。
しかし一方で、OSS には一定のリスクもあります。2025 年を振り返ると、npm パッケージにおけるサプライチェーン攻撃がたびたび話題になりました。
本記事では、サプライチェーン攻撃への対策の 1 つとして npm から pnpm へ移行した経緯とその過程で得た知見を共有します。
モチベーション
移行を決めた理由はいくつかありましたが、最大の理由は、先にも触れたサプライチェーン攻撃への対応です。
2025 年 8 月に発生した PhantomRaven と呼ばれる攻撃では、HTTP URL 経由で外部パッケージを動的に取得する手法が使われました。 これにより npm のセキュリティスキャンを回避し、クリーンなパッケージに見せかけることが可能になり、GitHub トークンや CI 認証情報などが窃取されました。
2025 年 9 月に発生した Shai-Hulud と呼ばれる攻撃では、npm のライフサイクルスクリプト (postinstall など) が悪用されました。 npm ではパッケージのインストール時にこれらのスクリプトが自動的に実行されます。 攻撃者はこの仕組みを利用し、インストール時に認証情報を窃取するスクリプトを仕込み、盗んだ npm トークンを使って他のパッケージにも感染を広げるワーム型の攻撃でした。
こうした脅威に対して、pnpm はライフサイクルスクリプトの実行をデフォルトで制限し、外部ソースからの依存関係をブロックする機能を提供しています。 これが移行を決断した大きな理由です。
pnpm が提供するセキュリティ機能
pnpm 移行にあたって以下の機能を活用しました。
strictDepBuilds
https://pnpm.io/settings#strictdepbuilds
pnpm では、依存パッケージのライフサイクルスクリプトがデフォルトで実行されません。
strictDepBuilds を true に設定すると、後述する allowBuilds で管理されていないライフサイクルスクリプトを持つ依存関係がある場合はインストールが失敗します。
strictDepBuilds: true
allowBuilds
https://pnpm.io/settings#allowbuilds
ライフサイクルスクリプトの実行を許可 / 禁止するパッケージを明示的に指定します。
pnpm 10.26 で追加された機能で、以前の onlyBuiltDependencies と ignoredBuiltDependencies を置き換えるものです。
strictDepBuilds と組み合わせることで、特定のパッケージのみがライフサイクルスクリプトを実行できるようになり、それ以外はインストールを失敗させる厳格な運用が可能になります。
allowBuilds: esbuild: true core-js: false
trustPolicy
https://pnpm.io/settings#trustpolicy
パッケージの信頼レベルとして以下がありますが、 trustPolicy を no-downgrade に設定すると、信頼レベルが以前のリリースよりも低下した場合にインストールを失敗させることができます。
- Trusted Publisher: GitHub Actions + OIDC トークン + npm provenance で公開
- Provenance: CI/CD システムからの署名付き証明書がある
- No Trust Evidence: ユーザー名/パスワードまたはトークン認証で公開
実際にプロダクトに導入すると、何らかのパッケージの依存関係でインストールが失敗することがあると思います。
その場合は後述する trustPolicyExclude で除外することが可能です。
trustPolicy: no-downgrade
trustPolicyExclude
https://pnpm.io/settings#trustpolicyexclude
trustPolicy のチェックから除外するパッケージを指定します。trustPolicy の要件を満たさない場合でも例外としてインストールを許可したいパッケージがある場合に使用します。
クラウドサインでは、trustPolicyExclude へのパッケージ追加は最小限にして、追加する場合は背景情報をコメントで補足するようにしています。
trustPolicyExclude: # コメント例: # sass の依存関係で "chokidar": "^4.0.0" が入っている # chokidar は 4.0.2 から attestations がなくなっているので除外している - chokidar@4.0.3
blockExoticSubdeps
https://pnpm.io/settings#blockexoticsubdeps
blockExoticSubdeps を true に設定すると、Git リポジトリ (git+ssh://...) や tarball URL (https://.../package.tgz) などの、npm レジストリ以外からの依存関係をブロックできます。
blockExoticSubdeps: true
実際の移行手順
移行は大まかに以下の流れで行いました。
pnpm importで既存のpackage-lock.jsonからpnpm-lock.yamlを生成 (npm で使っていたのと同じバージョンを維持するため)pnpm-workspace.yamlにセキュリティ設定を追加- Lefthook (git hooks) の更新
npm run→pnpm runnpx→pnpm exec
- GitHub Actions の更新
pnpm/action-setupアクションの追加actions/setup-nodeのキャッシュ設定をnpmからpnpmに変更npm ci→pnpm install --frozen-lockfile
移行してみての所感
思っていたよりもスムーズに移行できました。pnpm import のおかげで既存の package-lock.json をそのまま活用でき、依存関係の再解決による予期しない変更を避けることができました。
当初の目的であったサプライチェーン攻撃への対策という観点では、以下の点で効果を実感しています。
strictDepBuildsとallowBuildsにより、ライフサイクルスクリプトを実行するパッケージが明示的に管理されるようになったtrustPolicyにより、パッケージの信頼レベルが低下したパッケージの導入を防げるようになったblockExoticSubdepsにより、npm レジストリ以外からの依存関係が混入するリスクを排除できた
まとめ
npm パッケージを悪用したサプライチェーン攻撃への対策の 1 つとして、npm から pnpm へ移行しました。
pnpm ではさまざまなセキュリティ機能が提供されており、ライフサイクルスクリプトの制限や外部ソースからの依存関係のブロックが可能です。
さらに今後のアップデートでは今回紹介した strictDepBuilds や blockExoticSubdeps の設定がデフォルトで有効になりそうです。(参考 PR)
余談ですが、pnpm には minimumReleaseAge (公開から一定時間経過しないとインストールできない機能) もあります。
クラウドサインでは普段のパッケージのアップデートは Renovate で行っており、Renovate の minimumReleaseAge で同等の制御を担保しています。
これによりセキュリティパッチなど緊急のアップデートが必要な場合に手動で即座に対応できるようにしています。
セキュリティを重視したパッケージ管理に興味がある方は、ぜひ pnpm への移行を検討してみてください。
最後までお読みいただきありがとうございました。