はじめに
サプライチェーン攻撃が立て続けに発生しています。
- 3月19日: セキュリティスキャナTrivyのGitHub Actionsが侵害され、CI/CDパイプラインからクレデンシャルが窃取される
- 3月24日: Trivyの侵害を起点にPyPIのLiteLLMにも波及
- 3月31日: npmのaxiosのメンテナアカウントが乗っ取られ、RATが仕込まれたバージョンが公開
npmの場合、^1.14.0 のようなバージョン範囲指定がデフォルトなので、package-lock.json がなければ npm install のタイミング次第で悪意あるバージョンが自動的に入ってきます。だからこそnpmの世界ではロックファイルの利用が大前提になっているわけですが、.NETの場合はどうだっけか?と思ったので調べてみました。
NuGetの依存解決のしくみ
NuGetでは、参照するパッケージをPackageReferenceとしてバージョンを指定します。
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
範囲指定でバージョンを指定することも可能です。
<!-- 6.1以上の最小安定版を受け入れる -->
<PackageReference Include="ExamplePackage" Version="6.1" />
<!-- 6.x系の最新を受け入れるフローティングバージョン -->
<PackageReference Include="ExamplePackage" Version="6.*" />
<!-- 1.0以上2.0未満 -->
<PackageReference Include="ExamplePackage" Version="[1.0, 2.0)" />
ただし、.NETの場合はVisual StudioのNuGet Package Managerやdotnet add packageコマンドでパッケージを追加すると、その時点の最新安定版が固定で記録されます。このため、開発者が意図的に範囲指定やフローティングバージョンに書き換えない限り、直接依存するライブラリのバージョンは固定されます。
また、nuget.orgではパッケージの削除が許可されていないので、公開済みのバージョンを別の内容で上書きすることもできません。パッケージ署名の仕組みもあり、NuGetはnpmに比べてサプライチェーン攻撃に対する耐性が高い設計になっているといえます。
推移的依存は厳密に固定されるわけではないけれど、、、
プロジェクトAがパッケージBに依存していて、パッケージBがさらにパッケージCに依存している場合、プロジェクトAから見たパッケージCは推移的依存になります。.csproj に記録されるのは直接参照するパッケージのみです。そのパッケージが内部で依存している推移的パッケージのバージョンは、dotnet restore のたびにNuGetが依存関係の解決ルールにもとづいて決定されます。
.NETでもnpmと同じように packages.lock.json というロックファイルの仕組みを利用することができます。プロジェクトファイル(*.csproj)や、Directory.Packages.propsで次のように指定すると、各プロジェクトでロックファイルが有効になります。
<Project>
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>
この状態で、dotnet restore を実行すると、直接依存・推移的依存のバージョンが記載された packages.lock.json というロックファイルが生成されます。これをリポジトリにコミットして運用することで、ロックファイルと不一致なリストアをCIで検出することもできます。
下記はpackages.lock.json の抜粋です。直接的な依存である Asp.Versioning.Mvc と、推移的な依存である Asp.Versioning.Abstractions の両方のバージョンがロックファイルに記録されていることがわかります。また、contentHashも記録されているので、同じIDとバージョンのパッケージであっても中身が異なる(=改ざんされた)パッケージを検出することもできます。
{
"version": 1,
"dependencies": {
"net10.0": {
"Asp.Versioning.Mvc": {
"type": "Direct",
"requested": "[8.1.1, )",
"resolved": "8.1.1",
"contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==",
"dependencies": {
"Asp.Versioning.Http": "8.1.1"
}
},
"Asp.Versioning.Abstractions": {
"type": "Transitive",
"resolved": "8.1.0",
"contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA=="
}
}
}
}
この状態で、dotnet restore --locked-mode を実行すると、ロックファイルが存在しなかったり、不一致なパッケージがあるとエラーになります。CI/CDでこのオプションを指定しておけば、開発者がローカルで実行したときと異なるバージョンのパッケージが入ることを防止できます。
パッケージの依存関係が変わったのにロックファイルが更新されていない場合、次のエラーが発生するそうです(未確認)。
⇒ NU1004
error NU1004: The packages lock file is inconsistent with the project dependencies
so restore can't be run in locked mode.
ロックファイルの問題点と適用可能な最小バージョンルール
ただし、プロジェクトファイルごとにロックファイルが生成されるため、プロジェクト数が多いソリューションではコンフリクトが発生しやすくなる点に注意が必要です。
ここで重要なのが、NuGetの適用可能な最小バージョンルールです。NuGetは推移的依存を解決する際、要件を満たす中でもっとも低いバージョンを選択します。npmの場合、^1.2.3 は「1.2.3以上2.0.0未満の最新版」を取りに行くので、ロックファイルなしだとリストアのたびに異なるバージョンが入る可能性が高いですが、NuGetでは直接依存のバージョンが同じであれば、推移的依存も同じバージョンに解決されるケースがほとんどです。
下の例では、パッケージBはパッケージCのバージョン1.0.0以上を要求しています。パッケージCには1.1.0や1.2.0といった新しいバージョンもありますが、NuGetは適用可能な最小バージョンルールにしたがって、バージョン1.0.0を選択します。つまり、バージョン直指定 + 適用可能な最小バージョンルールの組み合わせにより、NuGetはロックファイルなしでも実質的に決定論的なリストアが実現できています。
じゃあ.NETはロックファイルは不要なのか?
一方で、2026年3月のTrivyやaxiosの事件を踏まえると、ロックファイルにはバージョン固定とは別の観点での価値があります。
content hashによる改ざん検出
packages.lock.json には各パッケージのcontent hashが記録されます。同じIDとバージョンだが中身が異なるパッケージ(=改ざんされたパッケージ)を検出できます。nuget.orgでは既存バージョンの上書きは許可されていないのでリスクは低いですが、プライベートフィードを利用している場合や、将来的にNuGetの署名検証を超える攻撃が発生した場合の防御層にはなりそうです。
SCAツールとの連携
Trivyなどの脆弱性スキャンツールは packages.lock.json を利用して推移的依存も含めたスキャンを実行します。ロックファイルがない場合は、Number of language-specific files num=0 と表示されて何もスキャンされません。
ただし、.NET 8以降では、dotnet restore時に依存パッケージの既知脆弱性をレポートする機能があるため、こちらの仕組みを利用するのも選択肢としてはありそうです。
脆弱性がある場合、次のように警告が表示されます。
XXXXXXX.csproj : warning NU1903: パッケージ 'AutoMapper' 13.0.1 に既知の 高 重大度の脆弱性があります、https://github.com/advisories/GHSA-rvv3-g6hj-g44x
まとめ
NuGetはバージョン直指定の文化と適用可能な最小バージョンルールにより、ロックファイルなしでも十分に再現可能なリストアが実現できています。通常の運用であればロックファイルを準備しなくても、今回のnpmのようなサプライチェーン攻撃は発生しにくい仕組みになっています。
一方で、ロックファイルにはcontent hashによる改ざん検出やSCAツール(Trivy等)との連携といった、バージョン固定とは別の価値があります。
サプライチェーン攻撃が日常化しつつある現在、とくにプライベートフィードを利用していたり、Trivyでの脆弱性スキャンを組み込みたい場合は、導入を検討する価値があると思います。
導入するかどうかの判断は、プロジェクトの規模やセキュリティ要件によって変わりますが、中小規模のプロジェクトなら運用負荷も低いので試してみてもよいかもしれません。
参考
- NuGetのロックファイルは使うべきなのか | tech.guitarrapc.cóm
- NuGet Package Version Reference | Microsoft Learn
- NuGet Package Dependency Resolution | Microsoft Learn
- NuGet PackageReference in project files | Microsoft Learn
- dotnet restore command | Microsoft Learn
- NuGet Error NU1004 | Microsoft Learn
- Central Package Management | Microsoft Learn
- Enable repeatable package restores using a lock file | .NET Blog