tech.guitarrapc.cóm

Technical updates

NuGetのtools/init.ps1は何がいやなのか

NuGetにはVisual Studioでパッケージをインストールするときに自動的に実行されるスクリプトの仕組みがあります、それがtools/init.ps1です。

前々からNuGetパッケージインストール時に警告なくスクリプトが実行されて嫌だなぁだと思っていましたが、今回はその理由を考えてみます。

init.ps1とは

NuGetパッケージにtools/init.ps1というスクリプトを配置することで、パッケージインストール時に任意の処理を実行させることができます。パッケージインストール後の追加処理を自動化する仕組みってことですね。

tools/init.ps1の現状

NuGetチームは2017年11月時点で以下のように非推奨コメントを出しています。

powershel script is deprecated for package reference, we don't recommend people to use script in their packages.

ref: https://github.com/NuGet/Home/issues/4318#issuecomment-343255043

ドキュメントにはPackageReference形式では実行されないと書かれています。しかしながら2025/1時点でもVisual Studioでtools/init.ps1が実行されます。

インストール方法 実行される
Visual Studioでパッケージインストール時
dotnet cliでパッケージインストール時 ×

init.ps1の実行を確認する

init.ps1を再度考えるきっかけになったFluentAssertionsでinit.ps1の実行状況を確認します。FluentAssertionsはPR #2943tools/init.ps1ファイルを追加したため、8.0.0をVisual StudioでインストールするとデフォルトブラウザでXceedのサイトが開きます。

image

Outputウィンドウを見ると、NuGetパッケージインストール時にスクリプト実行されているのがわかります。Executing script fileというログが実行タイミングを示しています。

Restoring packages for C:\Users\guitarrapc\source\repos\ConsoleApp4\ConsoleApp4\ConsoleApp4.csproj...
  GET https://api.nuget.org/v3-flatcontainer/fluentassertions/index.json
  OK https://api.nuget.org/v3-flatcontainer/fluentassertions/index.json 158ms
  GET https://api.nuget.org/v3-flatcontainer/fluentassertions/8.0.0/fluentassertions.8.0.0.nupkg
  OK https://api.nuget.org/v3-flatcontainer/fluentassertions/8.0.0/fluentassertions.8.0.0.nupkg 9ms
Installed FluentAssertions 8.0.0 from https://api.nuget.org/v3/index.json to C:\Users\guitarrapc\.nuget\packages\fluentassertions\8.0.0 with content hash qVCJIpukyFb9TO9W3vC4/sQF8lfrQksJbo4071uk3YfmHyKKlCZTTpyYhmCnnTZ2LVfFP1JgxM652nolCFjZDw==.
  CACHE https://api.nuget.org/v3/vulnerabilities/index.json
  CACHE https://api.nuget.org/v3-vulnerabilities/2025.01.15.06.48.40/vulnerability.base.json
  CACHE https://api.nuget.org/v3-vulnerabilities/2025.01.15.06.48.40/2025.01.17.11.29.03/vulnerability.update.json
Installing NuGet package FluentAssertions 8.0.0.
Generating MSBuild file C:\Users\guitarrapc\source\repos\ConsoleApp4\ConsoleApp4\obj\ConsoleApp4.csproj.nuget.g.props.
Writing assets file to disk. Path: C:\Users\guitarrapc\source\repos\ConsoleApp4\ConsoleApp4\obj\project.assets.json
Successfully installed 'FluentAssertions 8.0.0' to ConsoleApp4
Executing script file 'C:\Users\guitarrapc\.nuget\packages\fluentassertions\8.0.0\tools\init.ps1'...
Executing nuget actions took 1.26 sec
Time Elapsed: 00:00:02.0916149
========== Finished ==========

一方、dotnet cliからパッケージを追加しても実行されません。

$ dotnet add package FluentAssertions

Build succeeded in 0.4s
info : X.509 certificate chain validation will use the default trust store selected by .NET for code signing.
info : X.509 certificate chain validation will use the default trust store selected by .NET for timestamping.
info : Adding PackageReference for package 'FluentAssertions' into project 'C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\ConsoleApp5.csproj'.
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/index.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/index.json 183ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/page/1.3.0.1/4.19.3.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/page/1.3.0.1/4.19.3.json 483ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/page/4.19.4/8.0.0-rc.1.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/page/4.19.4/8.0.0-rc.1.json 472ms
info :   GET https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/page/8.0.0-rc.2/8.0.0.json
info :   OK https://api.nuget.org/v3/registration5-gz-semver2/fluentassertions/page/8.0.0-rc.2/8.0.0.json 448ms
info : Restoring packages for C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\ConsoleApp5.csproj...
info :   GET https://api.nuget.org/v3/vulnerabilities/index.json
info :   OK https://api.nuget.org/v3/vulnerabilities/index.json 407ms
info :   GET https://api.nuget.org/v3-vulnerabilities/2025.01.18.05.31.17/vulnerability.base.json
info :   GET https://api.nuget.org/v3-vulnerabilities/2025.01.18.05.31.17/2025.01.18.05.31.17/vulnerability.update.json
info :   OK https://api.nuget.org/v3-vulnerabilities/2025.01.18.05.31.17/vulnerability.base.json 148ms
info :   OK https://api.nuget.org/v3-vulnerabilities/2025.01.18.05.31.17/2025.01.18.05.31.17/vulnerability.update.json 478ms
info : Package 'FluentAssertions' is compatible with all the specified frameworks in project 'C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\ConsoleApp5.csproj'.
info : PackageReference for package 'FluentAssertions' version '8.0.0' added to file 'C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\ConsoleApp5.csproj'.
info : Generating MSBuild file C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\obj\ConsoleApp5.csproj.nuget.g.props.
info : Writing assets file to disk. Path: C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\obj\project.assets.json
log  : Restored C:\Users\guitarrapc\source\repos\ConsoleApp5\ConsoleApp5\ConsoleApp5.csproj (in 979 ms).

init.ps1ができること

init.ps1はPowerShellが提供する機能をVisual Studioの実行ユーザー権限で実行できます。基本的にWindowsでしか動作しないとはいえ、PowerShellはWindows組み込みシェルとして非常に強力なことは言うまでもありません。わかりやすい例を挙げると次のようなことができます。

  1. ファイルを操作
  2. レジストリの操作
  3. プロセスの操作

先のFluentAssertionsのinit.ps1スクリプトの場合、レジストリを読み取ってデフォルトブラウザで指定URLを開いています。1

  1. レジストリ'HKCU:\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice'を読み取って、デフォルトのブラウザを取得
  2. デフォルトブラウザならEdgeプロセスを開始
  3. Edgeでない場合はHKEY_CLASSES_ROOTからデフォルトブラウザのパスと実行コマンドを取得https://xceed.com/fluent-assertions/を開く

init.ps1の実行を止められるのか

NuGetパッケージをインストールする利用者側でinit.ps1実行を止める方法はないでしょうか。ドキュメントをみつつ動作確認をした結果、以下の通りです。

インストール方法 実行を止める設定
Visual Studio ×
nuget.config ×
PowerShellの実行ポリシー変更

Visual Studioで実行されるなら、そこで設定調整したいところですがVisual Studioの設定Options > NuGet Package Manager > Generalを見てもinit.ps1実行を止める設定はないです。残念。

image

nuget.configはnuget動作をパスごとに調整する手段ですが、設定を見ても特にPowerShell実行の制御は見当たりません。2

nuget.config reference | Microsoft Learn

力技ですが、PowerShellの実行ポリシー変更でスクリプト実行を止めるのが一番手っ取り早そうです。ただ、Restrictedはスクリプトが何も実行できなくなるので影響度高すぎます。Visual Studioでから開くPowerShellのポリシーをRestrictedにしてくれるとバランスいいので、Visual Studioにこの設定があるとよさそうです。

# これで防げるが強すぎる
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Restricted
# Visual Studioが現プロセスだけ設定するとよいのだが
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Restricted

init.ps1の懸念

以上の背景を把握した上で、init.ps1は何がいやなのか考えてみます。

NuGetはソフトウェアサプライチェインと脆弱性についてドキュメントを出しています。ここにはKnowing what is in your environment項目があげられているのですが、init.ps1の「インストール時に追加実行できるスクリプト」という性質は攻撃したいと思ったときに非常に都合がよいです。

背景1. tools/init.ps1の存在が事前にわからない

init.ps1の有無は、NuGet.orgやNuGetパッケージインストール時にわかりません。このため、特定パッケージでinit.ps1へ悪意あるスクリプトが追加されても、利用者はインストール後の実行開始まで気づけず、特に表面化しない処理なら実行に気づくことすらできないでしょう。少なくとも、私は気づける自信がありません。

init.ps1の実行内容はNuGet.orgを見てもわからず、.nupkgを解凍して中身を確認しないとわかりません。init.ps1は.nuspecに含まれていなくても、.nupkgに入っていれば実行されるのも気づきにくいことに拍車をかけています。リポジトリ上にtools/init.ps1が配置されていれば内容を確認できますが、GitHub Actionsなどあくまでもオープンなビルドプロセスを利用している時に限定されます。もしビルドプロセスがオープンでない場合、init.ps1は差し込み放題なのでnupkgを解凍するまで確認できません。

背景2. パッケージバージョンロックが普及していない

.NET界隈において、NuGetパッケージのバージョンはlockしたりピンする文化がこれまでなかったのも、問題が起こりやすいと考えています。以前の記事で紹介したManage Package Centrally移行時の推奨は「ピンニング有効」なのをみても、Microsoft推奨は「推移的パッケージ解決時のバージョンピンニング」に今後シフトしていくと予想していますが、それはかなり長期にわたる変化になるでしょう。これまでピンニングしていなかったのは、.NETが下位互換性を重視する文化、推移的依存の自動解決を重視、依存関係の複雑化を回避、などいろいろ察するところですが脆弱性の問題が表面化している今これらの文化が変わることを期待しています。3 デフォルトで無効なlockファイルも、こういうケースで有効にする意義を感じます。

init.ps1を使った攻撃は過去にすでに起こっている

2023年にinit.ps1を利用した攻撃がすでに起こっています。攻撃手法を説明してくれており、とても参考になりました。

Attackers are starting to target .NET developers with malicious-code NuGet packages

init.ps1の自動実行に関するIssueも作られているのですが、注目はされていないようです。

Warn package consumers when a package might automatically run code · Issue #12505 · NuGet/Home

Visual StudioのDevelop Communityでケースが作成されたので、よろしければVoteしてください。

Restrict the execution of init.ps1 from the NuGet package - Developer Community

そしてNuGetパッケージインストール後に任意の自動実行を求めるのはIssueを見る限り根強い要望があるようです。今後の変化は注目です。

init.ps1の代替手段

代替手段としては、GitHubなどでインストール後に追加でやってほしいコマンド処理を明示するのが緩和策になりそうです。経験上、ほとんどのパッケージインストール後に追加処理が必要ないと考えているので、わずかなケースのためにセキュリティリスクを高めるのはどうかという視点です。4

個人的にはパッケージインストール後に、自動的に任意の処理を実行できる仕組み自体が問題だと考えています。ルーズな自動処理5は攻撃しやすいポイントなので、パッケージインストール後に自動実行することをやめるしかないかなと。一方、ユーザーの意思で任意の処理を実行することを求めるのは数が多いと形骸化しますが、ほとんどのパッケージで追加処理が必要なケースはないと認識しています。

.NET Frameworkの時はinit.ps1ほしいシーンがまれにあった気もしますが、2025年現在、init.ps1の自動実行がないと困るケースは思いつかないです。何かあったかな、あったら教えてください。

まとめ

現時点のVisual Studioで、init.ps1の自動実行を止める手段は提供されておらず、そのNuGetパッケージにinit.ps1が含まれるか事前検証する仕組みも整備されていません。過去にこれを利用した攻撃が起こっていることから、今後もこの問題は続くでしょう。NuGetパッケージのセキュリティについては、今後も注目していきたいです。

FluentAssertionsが8.0.0でinit.ps1を追加したことで、この問題について再度考えるきっかけになりました。FluentAssertionsのinit.ps1は、デフォルトブラウザでXceedのサイトを開くだけのものでしたが、これが悪意あるスクリプトだったらどうなるか考えると、init.ps1の問題は深刻です。どうかブログを読んだ人は、init.ps1の利用を思いとどまってほしいところです。


  1. やっている処理自体はStart-Process "https://xceed.com/fluent-assertions/"で同じことができます。レジストリ読み込みなんていらない。
  2. ChatGPTはNuGet.configで止められるとか不正確な情報をいってくるのであぶない
  3. npmなどより依存関係の競合が少なかったので必要性が重視されなかったのもありそうです
  4. パッケージインストール後に追加の処理するのはパッケージ設計が悪い説もありますが、Unity含めていろんなシーンを考えるとちょっと潔癖すぎる気もするので難しいです
  5. init.ps1が実行されるかユーザーが把握できず、設定で制約もできないのはルーズと言わざるを得ない