npm依存関係のセキュリティ強化
原文は Alex O'Callaghan により に公開されました。 このブログを購読する
2026年3月30日、2つの悪意あるバージョンのaxiosがnpmに一時的に公開されました。axiosは週間ダウンロード数が1億件を超えるパッケージです。攻撃者はメンテナのアカウントを乗っ取り、隠された依存関係を含む[email protected]と[email protected]を公開しました。その依存関係のpostinstallフックが、クロスプラットフォーム対応のリモートアクセス型トロイの木馬(RAT)を密かにインストールする仕組みでした。
悪意あるバージョンは削除されるまで約3時間公開されたままで、Microsoftはこの攻撃を北朝鮮の国家アクターによるものとしています。エコシステムで最も広く使われているパッケージの一つを狙った、標的を絞った周到なサプライチェーン攻撃でした。
自分たちを守るために実際に何をしているのか、見直す良い機会です。
まずは基本から:lockfileを使う
言うまでもないことですが、あえて言っておきます。lockfileをコミットしてください。lockfileを迂回するようなインストールは実行しないでください。lockfileがあれば、前回解決されたものとまったく同じ内容がインストールされ、その日たまたまsemverの範囲を満たすバージョンが取得されることはありません。また、サプライチェーンの問題がdiffとして現れるため、推移的な依存関係が予期せず変わったことにも気づけます。
依存関係の数を減らす
サプライチェーン攻撃への露出を抑える最もシンプルな方法は、依存関係を減らすことです。インストールしないパッケージは侵害されることもありません。
まずは未使用の依存関係を洗い出しましょう。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証明付きで公開されていたパッケージについて、新しいバージョンでその証明が欠けている場合、pnpmがインストールをブロックします。今回のaxiosの攻撃は、正規リリースには存在する信頼されたパブリッシャーの紐付けなしに悪意あるバージョンが公開されていたため、この方法で検知可能でした。
一つ注意点があります。no-downgradeは、正規のパッケージのメンテナがprovenance証明をやめた場合に、時折誤検知を起こすことがあります。手動で検証した特定のパッケージについては、trustPolicyExcludeを使って除外できます。
trustPolicyExclude:
- "[email protected]"blockExoticSubdeps: trueも明示的に追加しておきましょう。これにより、推移的な依存関係がGitリポジトリや直接のtarball URLから解決されるのを防ぎ、レジストリからの取得に限定できます。
内部パッケージはスコープ付きにする
内部パッケージをプライベートレジストリに公開している場合は、スコープなしの名前ではなく、Organizationスコープ付き(例:@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レジストリチームに頼っています。安定性ウィンドウは、他の誰かが問題を見つけてくれて初めて機能します。追加する依存関係や取り込むアップグレードに対して注意を払うことに取って代わるものではありません。
記事をランダムに読む
コメント
ログインしてコメントする