こんにちは、セキュリティチームの坂梨です。 皆さん、ソフトウェアサプライチェーン攻撃対策はできていますか。
2025年の Shai-Hulud(大規模なnpmパッケージ侵害事件)以降、ソフトウェアサプライチェーン攻撃が話題に挙がる機会が増えました。特に今年3月末に発生した Trivy と axios の事件は大きく騒がれ、影響調査に追われた方も多いのではないでしょうか。 4月以降も lightning や TanStack などメジャーなライブラリが被害に遭っており、毎朝「さて今日は何が乗っ取られたのかな」とニュースを見る日々を過ごしています。
ライブラリが侵害されるたびに影響調査をするのは大変です。そこで、侵害されたライブラリをインストールするリスクを軽減するため、インストール時にクールダウンタイムを強制するプロキシ(公式レジストリの内容をフィルタして配信する社内用レジストリ)を作り、開発チーム内で運用を始めました。

はじめに
ライブラリの通常の脆弱性であれば、自社のリポジトリを調査して該当ライブラリが使われているかを確認すれば対応できます。しかし昨今のソフトウェアサプライチェーン攻撃では、開発者の端末をマルウェアに感染させるタイプのものが多くなっています。この場合、リポジトリの調査だけでは開発者がテスト的にインストールしたライブラリなどを追跡できず、端末のディスクをスキャンしたりネットワークログを追跡するなど、影響調査のハードルが高くなります。
昨今の攻撃頻度を考えると毎回の影響調査に工数をかけ続けることはスケールしません。そもそも、調査によって被害を小さくすることはできても、攻撃を防ぐことはできません。
ソフトウェアサプライチェーン攻撃の基本的な対策と課題
昨今の正規のライブラリを侵害するタイプのソフトウェアサプライチェーン攻撃は、検知・削除の効果を最大化するアプローチとして、新しすぎるバージョンをインストールしないことが攻撃対策の1つとして有効です。例えば npm には min-release-age という設定項目があり、リリース日から指定した日数が経過していないバージョンはインストールできない(クールダウンタイムを設ける)ようにできます。他のパッケージマネージャでも同様の設定ができるものが増えてきています。
ただしこの対策にも限界があります。
- パッケージマネージャによってはクールダウンタイムの設定に対応していないものもある
- 全開発者の端末でクールダウンタイムが設定されていることを保証するのが難しい
特に後者について、一度 ~/.npmrc 等にクールダウンタイムの設定をしたとしても、その設定が変更されていないか、個々のプロジェクトで上書きされていないかを管理するのは現実的ではありません。加えて、コーディングエージェントによってエンジニア以外が気軽にツールを開発したり、開発まで行かなくともnpxでMCPサーバを動かしたりすることが珍しくない昨今では、より一層管理が難しくなります。
クールダウンタイムの強制
そこで、リリースから一定時間が経っていないパッケージのバージョンを見えなくするプロキシ(社内レジストリ) tengen を作り、運用を始めています。パッケージの取得先を公式レジストリからこのプロキシに向けることでクールダウンタイムが強制されます。マルウェアスキャン機能などのサプライチェーン攻撃対策を組み込んだレジストリサービスやプライベートレジストリ構築用のソフトウェアもありますが、外部サービスへの依存を増やしたくなかったこと・場合によっては費用が嵩むことなどの理由から自作することにしました。
名前の由来は囲碁でも使われる「天元」で、万物の根源を意味します。このプロキシを経由して各プロジェクトにライブラリが広がっていくイメージです。
これによりクールダウンタイムに対応していないパッケージマネージャを利用している場合でも、レジストリの向き先を tengen に変えるだけで一律にクールダウンタイムを効かせられます。現時点では次のパッケージレジストリ・パッケージマネージャに対応しています。
- npm (npm/yarn/pnpm)
- PyPI (pip/uv/Poetry)
- RubyGems (Bundler)
- Maven (Maven/Gradle)
- Go Packages (Go Modules)
- Packagist (Composer)
ただし、プロキシだけでは、プロキシ経由でパッケージを取得する設定を全端末で行う必要があり、結局網羅性の担保が難しいです。
そこで、社内から公式レジストリやメジャーなミラーサーバへの通信をブロックする対策と組み合わせることで、用意したプロキシを経由しないとパッケージを取得できない状況、つまり強制的にクールダウンタイムが適用される状況を作り出しました。ネットワーク監視機器の仕様によっては、公式レジストリへの通信をブロックするのではなく、プロキシにリダイレクトさせるという方法も考えられます。
tengen がやること
npmなどのパッケージマネージャは、パッケージインストール時にレジストリへ「このパッケージにはどんなバージョンがあるか」を問い合わせ、バージョン一覧を含むメタデータを受け取って、依存関係を満たす最も新しいバージョンを選びます。
tengen がやることはシンプルで、このメタデータの問い合わせを公式レジストリへ中継しつつ、返ってきたバージョン一覧から 公開から一定時間が経っていないもの を取り除いてクライアントへ返すだけです。あわせて latest などのタグも、間引いたあとの一覧で最も新しいバージョンを指すように書き換えます。
こうすると、パッケージマネージャから見ればクールダウンタイムを過ぎていない新しいバージョンは「まだ公開されていない」のと同じ状態になります。パッケージマネージャは通常どおり「条件を満たす最新版」を選ぶだけで、自然とクールダウンを終えたバージョンがインストールされます。
この一連の流れを図にすると次のようになります。
sequenceDiagram
participant C as パッケージマネージャ<br/>(npm/pip/...)
participant T as tengen
participant R as 公式レジストリ
C->>T: メタデータ問い合わせ
T->>R: 問い合わせを中継
R-->>T: 全バージョン一覧を返す
Note over T: 公開から一定時間が<br/>経っていないバージョンを間引き<br/>latest などのタグも書き換える
T-->>C: 間引き後のバージョン一覧を返す
Note over C: 「条件を満たす最新版」を選択<br/>=クールダウンを終えたバージョン
C->>T: ソースコード類取得リクエスト
T->>R: リクエストを中継(tengenで中継しないことも可能)
R-->>T: ソースコード類
T-->>C: ソースコード類
また、オマケ程度の機能ではありますが、OSSFのマルウェアDB に登録されている既知の悪性パッケージもブロックするようにしています。
動かしてみる
使い方はシンプルです。tengenを起動してパッケージマネージャにレジストリを設定するだけです。-d オプションで遅延させる日数を指定できます。
$ npx tengen serve -d 7 # 7日間遅延させる ... tengen registry proxy started npm https://registry.npmjs.org pypi https://pypi.org rubygems https://rubygems.org go https://proxy.golang.org composer https://packagist.org maven https://repo.maven.apache.org/maven2 gradlePlugins https://plugins.gradle.org/m2 delay: 7 day(s) passthrough: redirect malicious: /var/folders/73/bk9tj_ln0hg898zbrc6t9hj40000gp/T/tengen-malicious-db.json listening: http://127.0.0.1:3000
この状態でnpmの @aws-sdk/client-s3 の情報を参照してみましょう。
まずはレジストリを変更せずに実行すると、latest タグが2026/6/23時点の最新バージョンである 3.1074.0 を指しています。
$ npm info @aws-sdk/client-s3 ... dist-tags: latest: 3.1074.0 rc: 3.5.1-rc.0 gamma: 1.0.0-gamma.11 beta: 1.0.0-beta.5 alpha: 1.0.0-alpha.21
続いて、レジストリを tengen に向けて再度実行してみます。
ここでは --registry でレジストリの設定を変更していますが、実運用では .npmrc に設定を書いておけば毎回コマンドライン引数を渡す必要はありません。
$ npm info @aws-sdk/client-s3 --registry=http://localhost:3000/npm # tengenに向ける ... dist-tags: latest: 3.1069.0 rc: 3.5.1-rc.0 gamma: 1.0.0-gamma.11 beta: 1.0.0-beta.5 alpha: 1.0.0-alpha.21
すると、 latest タグが 3.1069.0 を指すようになりました。
同様に、npm install も実行してみると、最新の 3.1074.0 ではなく 3.1069.0 がインストールされます。
$ npm install @aws-sdk/client-s3 --registry=http://localhost:3000/npm # tengenに向ける $ npm ls @aws-sdk/client-s3 npm@ /path/to/project └── @aws-sdk/client-s3@3.1069.0
tengen を通すことで、クールダウンタイムが強制的に適用されます。
tengenの課題
クールダウンタイムを設けることで、サプライチェーン攻撃対策としては一定の効果が見込めます。しかし、リリースを遅らせることは脆弱性パッチの適用も遅れることを意味します。そのため、脆弱性パッチの場合はクールダウンタイムを無視してアップデートすることが望ましいです。tengenには許可リストの機能も持たせており、特定のパッチだけをクールダウンタイムを無視してインストールさせることも可能です。
ただし、場合によってはパッチを当てたいライブラリが依存するライブラリや、さらにその先のライブラリも許可リストに追加する必要があるなど、運用ハードルは若干高めです。また、axiosの事例のようにライブラリの正規のコミッターがアカウントを乗っ取られた場合、偽の脆弱性情報を報告することも不可能ではないため、脆弱性情報を100%信頼して自動で許可リストに追加することにはリスクもあります。
このあたりは運用を進めていく中でより良い方法を模索していく必要があると感じています。
まとめ
サプライチェーン攻撃の多くは公開直後の短い時間で検知・削除されることから、「公開から一定時間経っていないバージョンは存在しないことにする」プロキシ tengen を作って運用を始めました。新しすぎるバージョンを一覧から間引くだけのシンプルな仕組みですが、サプライチェーン攻撃への基本的な備えとして十分機能しています。
脆弱性パッチの運用との兼ね合いなど改善していきたい点はまだ残っているので、今後も運用しながら対策を模索していきます。
なお、今回はサプライチェーン攻撃対策としてクールダウンタイムに着目しましたが、npm系の場合は postinstall などのインストール時スクリプトに依存していないプロジェクトであれば ignore-scripts=true も併用すると、インストール時に任意スクリプトが実行されるリスクを抑えられるためおすすめです。
We are Hiring!
エムスリーではエンジニアを絶賛募集しています! 少しでもご興味をお持ちの方は、ぜひカジュアル面談等にご応募ください!