サプライチェーン攻撃からシステムを守る。pnpm / uv / Composerを前提とした実践的防御と導入プロセス

目次

はじめに

こんにちは。RSSでクラウド基盤アーキテクトをしているS.R.です。
OSSパッケージは現代の開発に欠かせないインフラですが、JavaScript / TypeScript では npm registry、Python では PyPI、PHP では Packagist から、多くの依存パッケージを日常的に取り込んでいます。

一方で、この便利さはそのまま攻撃面にもなります。攻撃者にとって、人気パッケージやその依存関係を侵害できれば、開発者のローカル環境、CI/CD、場合によっては本番環境にまで到達できます。あなたがローカルPCで何気なく npm installuv sync を実行した瞬間、あなたのPC内にある ~/.aws/credentials.env の中身が密かに攻撃者のサーバーへ送信される、といった事態が実際に起こり得るのです。

近年目立つのは、単なるタイポスクワッティングだけではありません。正規パッケージの公CI環境でビルドエラーが続出した開権限を奪う、メンテナーアカウントを侵害する、悪性依存を差し込む、preinstall / postinstall などの lifecycle script (※パッケージのインストール前後に自動実行されるスクリプト)でインストール時にコードを実行する、といった攻撃が増えています。

たとえば Shai-Hulud では、侵害された npm メンテナーアカウント経由で人気 JavaScript パッケージに悪性 post-install script が注入され、シークレット窃取と自己増殖を組み合わせた攻撃が発生しました。

また、Bitwarden CLI の事案では、@bitwarden/cli@2026.4.0 に悪意ある preinstall フックが追加され、npm install 時に情報窃取マルウェアが実行され得る状態だったと報告されています。–ignore-scripts 相当の設定なしで install していた場合、インストール時点でペイロードが発火し得る点が問題でした。

Axios の事案では、悪性バージョン axios@1.14.1 と axios@0.30.4 が npm registry に公開され、悪性依存 plain-crypto-js@4.2.1 を通じて multi-stage payload や remote access trojan を展開する攻撃が確認されています。

この記事では、JavaScript / TypeScript は pnpm v10 / v11、Python は uv、PHP は Composer を前提に、パッケージマネージャー経由のサプライチェーン攻撃に対する実践的な防御策を整理します。

【前提環境・動作確認バージョン】 本記事で解説する設定やコマンドは、以下の環境・バージョンでの検証を前提としています。パッケージマネージャーのセキュリティ機能はバージョンによってデフォルトの挙動が異なる場合があるため、お使いの環境に合わせてご確認ください。

  • pnpm:v10.26.0 以降、および v11
  • uv:0.11.7
  • Composer:2.9.7
  • CI環境:Ubuntu 24.04(GitHub Actions ubuntu-latest など)

まず押さえるべき攻撃パターン

パッケージマネージャー経由の攻撃は、ざっくり次のように分解できます。

攻撃パターン内容主な対策
タイポスクワッティング似た名前の悪性パッケージを公開する依存追加レビュー、許可リスト、プライベートレジストリ / 社内プロキシ
正規パッケージの乗っ取りメンテナーアカウントや publish token を侵害する有名パッケージでも blind trust しない
悪性依存の差し込み正規パッケージに悪性依存を追加するlockfile レビュー、依存差分チェック
lifecycle script 悪用preinstall / install / postinstall でコード実行するscript 無効化、allowlist、CI 隔離
CI/CD 経路の悪用build / release workflow や secret を悪用するsecret 最小化、環境分離、権限分離
exotic dependency 悪用git URL や tarball URL から直接コードを取得する(※パッケージマネージャーの正規レジストリを経由しない依存)推移的依存の exotic source 禁止
アカウント回復経路の悪用失効ドメインや古いメール経由でアカウントを奪う依存元の健全性確認、重要依存の棚卸し

重要なのは、これらの攻撃が「依存パッケージを追加した瞬間」だけで成立するわけではない、という点です。

CI/CD で install した瞬間、lockfile を更新した瞬間、悪性 CLI を実行した瞬間にも被害は発生します。

また、被害は secret の窃取に限られません。サプライチェーン攻撃の本質は、信頼して install した処理の中で任意コードが実行されることです。そのため、RAT(Remote Access Trojan: 遠隔操作マルウェア)、バックドア、クリプトマイナー、追加ペイロードの取得、端末内ファイルの改ざん・削除、横展開なども起こり得ます。

基本方針

パッケージマネージャー経由のサプライチェーン攻撃に対して、「この設定を入れれば安全」という単一の対策はありません。
標準ポリシーとしては、次の4つを重ねるのが現実的です。

  1. lockfile を必ず使い、CI/CD では lockfile を更新させない
  2. install 時に任意コードを実行させない
  3. 公開直後のパッケージをすぐに取り込まない
  4. 公開経路や trust level が下がった更新を検知する

このうち、特に重要なのが lifecycle script の制御と Dependency Cooldown です。

npm / pnpm 系では、preinstall、install、postinstall などの lifecycle script が攻撃の発火点になりがちです。pnpm は supply chain attack 対策として、公開直後のバージョンを遅延させる minimumReleaseAge や、信頼度の低下を検知する trustPolicy を提供しています。
また、pnpm の minimumReleaseAge は、公開から一定時間が経過するまで install 対象から外すことで、公開直後の悪性バージョンを踏むリスクを下げるための設定です。

攻撃と防御のモデル図

パッケージマネージャー経由のサプライチェーン攻撃は、「悪性パッケージが存在すること」だけで成立するわけではありません。
実際には、公開直後のバージョンをすぐに取り込んでしまうこと、install 時に lifecycle script を実行してしまうこと、その環境に secret や権限が存在することが重なって被害になります。
以下は、Dependency Cooldown と script 実行制御が、どのように攻撃を止めるかを表した図です。

Dependency Cooldown は「公開直後の悪性バージョンをそもそも取り込まない」ための防御です。
lifecycle script の制御は「万一悪性パッケージを取得しても、install 時の発火を止める」ための防御です。
両者は役割が異なるため、片方だけではなく組み合わせて使う必要があります。

npm の –ignore-scripts は強力だが、pnpm では allowlist 化を標準にする

npm を使う場合、CI/CD では次のように lifecycle script を無効化できます。

npm ci --ignore-scripts

npm の ignore-scripts は、package.json に定義された scripts を実行しない設定です。ただし、npm start、npm test、npm run のように明示的に script を実行するコマンドは、その対象 script 自体を実行します。

一方、pnpm を標準にするなら、npm の –ignore-scripts をそのまま中心に置くよりも、必要な build script だけを allowlist 化する方針が適しています。

Tip!

CI/CD では、依存パッケージの lifecycle script を原則実行しない。
ただし、@prisma/client、esbuild、sharp など、正当な理由で build script が必要なものだけをレビューしたうえで明示的に許可します。

pnpm v10 の推奨設定

pnpm v10 を使う場合は、できれば v10.26.0 以降 を前提にします

理由は、minimumReleaseAge は v10.16.0、trustPolicy は v10.21.0、blockExoticSubdeps は v10.26.0 で追加されているためです。
推奨する pnpm-workspace.yaml は次のとおりです。

minimumReleaseAge: 10080 # 7 days

strictDepBuilds: true
dangerouslyAllowAllBuilds: false

allowBuilds:
  esbuild: true
  "@prisma/client": true
  sharp: true

trustPolicy: no-downgrade
blockExoticSubdeps: true

minimumReleaseAge は、パッケージ公開から指定した分数が経過するまで、そのバージョンを install 対象にしない設定です。
pnpm では単位が 分 なので、1日は 1440、7日は 10080 です。v11 ではデフォルトが 1440、v11 より前では 0 とされています。
strictDepBuilds: true は、未レビューの build script を持つ依存がある場合に install を失敗させるための設定です。pnpm v10 では標準動作が v11 と異なるため、CI/CD では明示的に true を指定しておくのが安全です。

pnpm v11 の推奨設定

pnpm v11 では、サプライチェーン対策がより標準寄りになります。
特に大きいのは、minimumReleaseAge のデフォルトが 1440、つまり1日になる点です。
v11 向けの推奨設定は次のとおりです。

minimumReleaseAge: 10080 # 7 days

strictDepBuilds: true

dangerouslyAllowAllBuilds: false

allowBuilds:

  esbuild: true

  "@prisma/client": true

  sharp: true

trustPolicy: no-downgrade

blockExoticSubdeps: true

# private registry / mirror が time field を返さない場合は要検証

minimumReleaseAgeIgnoreMissingTime: false

blockExoticSubdeps: true は、推移的依存が git URL や tarball URL のような exotic source を使うことを防ぐ設定です。
minimumReleaseAgeIgnoreMissingTime: false は、registry metadata に time field がないパッケージをどう扱うかの設定です。より厳格にするなら false にできますが、private registry や mirror が time field を返さない場合は解決に失敗する可能性があるため、導入前に検証が必要です。

Warning!

minimumReleaseAgeIgnoreMissingTime: false は厳格な設定ですが、社内 registry / mirror の実装によっては install が失敗する可能性があります。
導入前に代表的なリポジトリで検証してください。

Dependency Cooldown:npm と pnpm と uv で単位が違う

npm v11 以降には min-release-age があります。これは、指定した日数より新しいバージョンを install 対象から外す設定です。npm では単位が 日 です。

min-release-age=7

pnpm の minimumReleaseAge は 分 単位です。

minimumReleaseAge: 10080 # 7 days

uv の exclude-newer は、指定した timestamp または “24 hours”、”1 week” のような duration より新しい distribution artifact を依存解決対象から除外します。

[tool.uv]

exclude-newer = "1 week"

整理すると次のようになります。

ツール設定単位7日相当
npm v11min-release-age7
pnpm v10 / v11minimumReleaseAge10080
uvexclude-newertimestamp / duration“1 week”

サプライチェーン攻撃が活発な状況では、標準値は7日、急ぎの更新が多いプロジェクトでも最低3日程度を推奨値にすると適切です。
もちろん、cooldown は万能ではありません。すでに lockfile に悪性バージョンが入っている場合や、攻撃が長期間検知されない場合は防げません。それでも、公開から数時間でテイクダウンされるような攻撃には有効です。

trustPolicy: no-downgrade は有効だが、万能ではない

pnpm の trustPolicy: no-downgrade は、過去のリリースより trust level が下がったパッケージの install を失敗させる設定です。

trustPolicy: no-downgrade

ここでいう trust level は、利用者側が「そのパッケージがどのような経路で publish されたか」を判断するための材料です。つまり、公開者側の運用を細かく説明するためではなく、利用者側で怪しい更新を検知するためのシグナルとして扱います。

一方で、限界もあります。Bitwarden CLI の事案のように、Trusted Publishing 経路そのものが悪用された場合は trust level が下がらないため、trustPolicy: no-downgrade だけでは検知できません。

つまり、trustPolicy は強力な補助線ですが、単独ではなく、次の対策と組み合わせる必要があります。

  • lifecycle script の allowlist 化
  • minimumReleaseAge による cooldown
  • lockfile の固定
  • CI/CD secret の分離
  • install 時・実行時の外向き通信監視

Python は uv を標準にする

Python 側では uv を前提にします。
CI/CD では uv.lock をコミットし、lockfile が更新されないことを前提に環境を同期します。

uv lock --check

uv sync --locked --no-dev

uv では locking と syncing が自動で行われますが、–locked を使うと lockfile が古い場合に更新せずエラーにできます。また、uv lock –check は lockfile が project metadata と一致しているかを確認するためのコマンドです。
uv 側でも、公開直後の artifact を避ける設定ができます。

exclude-newer = "1 week"

no-build = true

exclude-newer は、指定した時点より新しい distribution artifact を依存解決対象から除外する設定です。”24 hours” や “1 week” のような duration も指定できます。

no-build = true は source distribution の build を禁止する設定です。有効にすると、解決中に任意の Python コードを実行せず、build が必要な distribution はエラーになります。

legacy な requirements.txt 運用が残る場合は、hash pinning も併用できます。

[tool.uv.pip]

require-hashes = true

no-build = true

require-hashes は、すべての requirement に hash と exact pin を要求するモードです。uv の hash-checking mode は all-or-nothing で、すべての requirement に hash が必要になります。

PHP (Composer編)

PHP では Composer を前提にします。

Composer は npm / pnpm と少し脅威モデルが違います。Composer の scripts は、root package の composer.json に定義されたものだけが実行され、依存パッケージ側の scripts は実行されません。
そのため、PHP では依存パッケージの postinstall script よりも、次を重点的に管理します。

  • composer.lock による依存固定
  • Composer plugin の allowlist 化
  • root package の scripts の制御
  • composer audit による既知脆弱性チェック
  • private repository / plugin / scripts のレビュー
  • HTTP や TLS 無効化の禁止

アプリケーションでは composer.lock を必ずコミットします。CI/CD では次のように固定します。

composer validate --strict

composer install \

  --no-dev \

  --prefer-dist \

  --no-interaction \

  --no-progress \

  --no-scripts \

  --audit

composer audit --locked

–no-scripts は composer.json に定義された scripts の実行をスキップするオプションです。

composer audit –locked は、現在の vendor/ ではなく lockfile 上のパッケージを監査します。

composer.json には、少なくとも次のような設定を入れます。

{

  "config": {

    "allow-plugins": {

      "composer/installers": true,

      "dealerdirect/phpcodesniffer-composer-installer": true,

      "symfony/flex": true,

      "*": false

    },

    "preferred-install": {

      "*": "dist"

    },

    "secure-http": true,

    "disable-tls": false,

    "audit": {

      "block-insecure": true,

      "block-abandoned": true

    },

    "sort-packages": true

  },

  "minimum-stability": "stable",

  "prefer-stable": true

}

Composer 2.2.0 以降の allow-plugins は、Composer 実行中にコードを実行できる plugin を制限するための設定です。デフォルトは {} で plugin は許可されず、信頼した plugin だけを明示的に許可できます。true で全 plugin を許可することもできますが、これは推奨されません。

Composer には pnpm の minimumReleaseAge や uv の exclude-newer に相当する標準的な cooldown 設定はありません。そのため、PHP では cooldown を Composer 単体ではなく、Dependabot / Renovate、Private Packagist、artifact proxy、mirror 側の運用で実現します。

標準ポリシーの全体像

個別の設定だけを見ると、pnpm、uv、Composer はそれぞれ別々の話に見えます。

しかし、実際にはどれも「lockfile 固定」「任意コード実行の抑止」「公開直後バージョンの回避」「CI/CD での強制」という同じ原則に沿っています。

重要なのは、これらを「推奨設定」として 社内ドキュメントやガイドラインに書くだけで終わらせず、CI/CD の標準ポリシーとして強制することです。
サプライチェーン攻撃は、個人の注意ではなく、仕組みで止める必要があります。

CI/CD で標準化したいポリシー

最終的には、個々の開発者の注意に依存せず、CI/CD と repository 設定で強制します。
JavaScript / TypeScript では、CI は次のように固定します。

pnpm install --frozen-lockfile

pnpm-workspace.yaml は次を標準にします。

minimumReleaseAge: 10080

strictDepBuilds: true

dangerouslyAllowAllBuilds: false

trustPolicy: no-downgrade

blockExoticSubdeps: true

allowBuilds:

  esbuild: true

  "@prisma/client": true

  sharp: true

Python では、CI を次のように固定します。

uv lock --check

uv sync --locked --no-dev

pyproject.toml には次を入れます。

[tool.uv]

exclude-newer = "1 week"

no-build = true

PHP では、CI を次のように固定します。

composer validate --strict

composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts --audit

composer audit --locked

ただし、no-build = true や –no-scripts は、正当な build / generate 処理を壊す可能性があります。例外が必要な場合は、対象パッケージ・用途・代替可否をレビューし、例外リストとして管理します。

Caution!

no-build = true や --no-scripts を無理に全プロジェクトへ適用すると、正当な生成処理まで止める可能性があります。

例外を許可する場合は、理由・対象パッケージ・代替可否を明記してレビュー対象にしてください。

【FAQ】開発者向けトラブルシューティング

セキュリティ設定を厳格化すると、日々の開発においてパッケージのインストールがブロックされるケースが発生します。よくあるつまずきと対処法は以下の通りです。

Q. CIで strictDepBuilds(または no-build)エラーが出てインストールに失敗します。
A. 追加したパッケージがC拡張のコンパイルなど正当なビルド処理を行っているか確認してください。問題がなければ、pnpm-workspace.yaml の allowBuilds(等)にパッケージを追加し、インフラ・セキュリティ担当者へレビューを依頼してください。

Q. minimumReleaseAge(または exclude-newer)に引っかかって最新版のパッケージが入れられません。
A. 原則として数日間のCooldown期間を待つのが安全です。緊急のバグ修正版などで今すぐ必要な場合に限り、設定期間を一時的に短縮する、あるいは担当チームへ例外対応を相談してください。

インシデント時の初動

悪性パッケージを install した疑いがある場合、依存バージョンを戻すだけでは不十分です。lifecycle script が実行された可能性があるなら、その環境は侵害された前提で扱います。
secret の漏洩だけでなく、マルウェア感染、永続化、外部通信、追加ペイロードの取得、端末内ファイルの改ざん・削除、横展開の可能性も考慮します。

優先すべき対応は次のとおりです。

  1. lockfile と package manager cache に悪性バージョンが残っていないか確認する
  2. 影響を受けた開発端末・CI runner をネットワークから隔離する
  3. EDR / AV / フォレンジック観点で、不審プロセス、永続化設定、外部通信、追加ファイルを確認する
  4. GitHub token、npm token、SSH key、cloud credential、.env、CI/CD secret をローテーションする
  5. 不審な repository、branch、GitHub Actions workflow、release、package publish 履歴を確認する
  6. artifact proxy や registry cache から悪性 artifact を削除する
  7. 安全なバージョンへ pinning する
  8. 必要に応じて開発端末・CI runner を再イメージ化する
  9. 再発防止として cooldown、script allowlist、lockfile enforcement を導入する
Warning!

install 時に任意コードが実行された可能性がある場合、その環境は侵害された前提で扱います。
secret 漏洩だけでなく、マルウェア感染、永続化、外部通信、追加ペイロードの取得、横展開の可能性も考慮します。
「依存バージョンを戻したので完了」ではなく、端末・runner の隔離、侵害調査、token rotation、cache 削除、必要に応じた再イメージ化まで含めて対応します。

まとめ

この記事の対象は、パッケージを公開する側ではなく、日々の開発・CI/CD で依存パッケージを利用する側です。
そのため重要なのは、「安全なパッケージだけを信じる」ことではありません。悪性パッケージが混入しても、被害を抑える仕組みを標準化することです。


サプライチェーン攻撃の本質は、「悪性パッケージが secret を盗むこと」ではなく、「信頼して install した処理の中で任意コードが実行されること」です。
そのため被害は token や .env の窃取に限らず、RAT、バックドア、クリプトマイナー、追加ペイロードの取得、端末内ファイルの改ざん・削除、横展開などにも広がり得ます。

実運用では、次の状態を標準にする必要があります。

  • lockfile を必ずコミットする
  • CI/CD では lockfile を更新させない
  • lifecycle script は原則無効化し、必要なものだけ許可する
  • 公開直後のパッケージは cooldown 期間を置く
  • Trusted Publishing や provenance の trust level 低下を検知する
  • CI/CD の secret は最小権限・短命・分離を徹底する
  • install 時に任意コードが動いた場合は、secret 漏洩だけでなく、端末・CI runner の侵害前提で対応する

pnpm、uv、Composer は、それぞれ異なる形でこれらの対策を実現できます。

  • pnpm では、minimumReleaseAge、allowBuilds、strictDepBuilds、trustPolicy、blockExoticSubdeps を組み合わせます。
  • uv では、uv.lock、–locked、exclude-newer、no-build、必要に応じて require-hashes を組み合わせます。
  • Composer では、composer.lock、allow-plugins、–no-scripts、composer audit –locked を組み合わせます。

重要なのは、これらを「推奨」ではなく CI/CD の標準ポリシーとして強制することです。サプライチェーン攻撃は、個人の注意力ではなく、仕組みで止める必要があります。

最後まで読んでくださり、ありがとうございました。安全なインフラ環境の構築に向け、少しでも皆様の参考になれば嬉しいです。
これからもRSSのメンバーがブログを投稿していきますので、ぜひチェックしてください!

レイスシステムソリューションズ株式会社のソフトウェア開発や、
採用に関するお問い合わせについては、下記のリンクにてお問い合わせください。

お問い合わせ