npm依存関係のセキュリティ強化
2026年3月30日、axiosの悪意ある2つのバージョンがnpmに一時公開されました。axiosは週間ダウンロード数が1億回を超えるパッケージです。攻撃者はメンテナのアカウントを乗っ取り、[email protected]と[email protected]を公開しました。いずれも隠された依存関係を含んでおり、そのpostinstallフックがクロスプラットフォーム対応のリモートアクセス型トロイの木馬(RAT)を密かにインストールする仕組みでした。
悪意あるバージョンは削除されるまで約3時間にわたって公開されたままでした。Microsoftはこの攻撃を北朝鮮の国家主体によるものとしています。エコシステムで最も広く使われているパッケージの一つを狙った、標的型で周到に準備されたサプライチェーン攻撃でした。
自分たちを守るために実際に何をしているのか、見直す良い機会です。
まずは基本から:ロックファイルを使う
当たり前のことですが、あえてお伝えします。ロックファイルは必ずコミットしてください。ロックファイルを迂回するインストールは実行しないでください。ロックファイルがあれば、前回解決された内容とまったく同じものがインストールされます。今日のsemverの範囲を満たす適当なバージョンが入ることはありません。また、サプライチェーン上のインシデントが差分として現れるため、推移的な依存関係が予期せず変わったときに気づくことができます。
依存関係の露出面を減らす
サプライチェーン攻撃への露出を抑える最もシンプルな方法は、依存関係を減らすことです。インストールしていないパッケージが侵害されることはありません。
まずは未使用の依存関係を監査しましょう。Knipはコードベースをスキャンし、package.jsonには記載されているものの、どこからもimportされなくなったパッケージを洗い出してくれます。プロジェクトには時間が経つにつれて使われなくなった依存関係が溜まっていきますが、積極的に整理しているチームは多くありません。Knipを定期的に実行したり、CIパイプラインに組み込んだりすることで、不要な依存関係を確実に取り除けます。
次に、e18eにも注目してください。JavaScriptパッケージのクリーンアップやモダナイズ、パフォーマンス改善に取り組むエコシステムのイニシアチブです。活動の一環として、依存関係として存在する意味のないis-oddのようなパッケージや、すでにネイティブで代替できるlodashの関数などを、より軽量でモダンな代替に置き換えることが進められています。依存関係が軽くなれば、気にすべきパッケージも減ります。
pnpmの設定
今回のaxiosのRATは、パッケージのインストール時に自動実行されるpostinstallフックを介して仕込まれました。これはnpmのサプライチェーン攻撃の大多数で使われている手口です。
pnpm v10では、依存関係のpostinstallスクリプトの自動実行がデフォルトで無効になっています。要求してきたパッケージのビルドスクリプトを片っ端から実行するのではなく、正当に必要なものだけを明示的に許可リストに登録します。
# pnpm-workspace.yaml
allowBuilds:
esbuild: true
"@parcel/watcher": trueこれまでビルドスクリプトを必要としなかった依存関係が、突然スクリプトを実行することはありません。許可リストに入っていないpostinstallフックであれば、侵害されたバージョンのパッケージが悪意あるコードを実行するために使おうとしても実行されません。
保護を強化するために、次のpnpm設定も有効にしておくことをおすすめします。
minimumReleaseAge: 10080 # 7 days in minutes
trustPolicy: no-downgrade
blockExoticSubdeps: trueminimumReleaseAgeは、指定した分数以内に公開されたパッケージバージョンのインストールをpnpmに拒否させる設定です。今回のaxiosの攻撃は3時間公開されていました。1日(1440)の遅延を設けていれば、十分に回避できていたはずです。私たちはRenovateの安定性ウィンドウに合わせて7日間に設定しています。
trustPolicy: no-downgradeを設定すると、以前は信頼できるCIパイプラインによる来歴証明(provenance attestation)付きで公開されていたパッケージの新バージョンで、その証明が欠けている場合、pnpmがインストールをブロックします。今回のaxiosの攻撃は、正規リリースにあった信頼できるパブリッシャーの紐付けなしで悪意あるバージョンが公開されていたため、この方法で検知可能でした。
ただし注意点があります。no-downgradeは、正規のパッケージのメンテナが来歴証明をやめた場合に、時折誤検知を起こすことがあります。手動で検証済みのパッケージについては、trustPolicyExcludeを使って除外できます。
trustPolicyExclude:
- "[email protected]"また、blockExoticSubdeps: trueも明示的に追加してください。これにより、推移的な依存関係がgitリポジトリや直接のtarball URLから解決されるのを防ぎ、必ずレジストリ経由で取得されるように強制できます。
社内パッケージはスコープで管理する
プライベートレジストリに社内パッケージを公開している場合は、スコープなしの名前ではなく、組織スコープ(例:@myorg/package-name)配下で公開するようにしてください。これにより、攻撃者が社内パッケージと同名または類似名のパッケージをパブリックに公開する依存関係混乱(dependency confusion)攻撃のリスクを低減できます。レジストリ設定が意図せず後退したり、新しい環境でパブリックレジストリを優先して取得するように誤設定されたりした場合、スコープなしの社内パッケージ名は格好の標的になります。
Renovateによる自動アップグレード
私たちはプロジェクト横断で依存関係の更新をRenovateで管理しています。ここでは2つの設定が連携して機能します。
minimumReleaseAge(旧称stabilityDays)は、新しいパッケージバージョンが公開されてから一定日数が経つまで、RenovateがPRを作成するのを遅延させます。私たちはこれを7日間に設定しています。これにより、悪意あるリリースがコードベースに取り込まれる前にコミュニティが検知する時間を確保できます。また、公開から2日後にパッチが出るようなリリースを拾ってしまう無駄なchurnも避けられます。
セキュリティアップデートのPRはminimum release ageを完全にバイパスします。Renovateが依存関係に既知の脆弱性を検知した場合、修正がいつ公開されたかにかかわらず、即座にPRを作成します。安定性のための遅延がCVEへの対応を遅らせることはありません。遅くなるのは通常のバージョンアップだけです。
社内パッケージについては、遅延なしの別パッケージルールを設定してください。統合時の問題を早期に発見できるよう、素早く展開したいからです。
{
"minimumReleaseAge": "7 days",
"packageRules": [
{
"matchPackagePrefixes": ["@myorg/"],
"minimumReleaseAge": "0 days"
}
]
}万能薬はありません
これらの設定は、何かを自ら検知するのではなく、時間を稼ぐことで機能します。私たちは、侵害されたパッケージの特定をセキュリティ研究者や自動スキャンプラットフォーム、オープンソースコミュニティに頼っており、迅速な削除はnpmレジストリチームに依存しています。安定性のためのウィンドウは、誰かが問題を見つけてくれて初めて効果を発揮します。追加する依存関係や取り込むアップグレードに対して注意を払うことに代わるものではありません。
記事をランダムに読む