はじめに
生成AIがコードを書く場面が増えました。実装の速度は上がりましたが、私が気になっているのは別の点です。作ったものに脆弱性が見つかったとき、誰がどこまで責任を持って対応できるのか、という点です。
技術選定の場では、「この技術は安全か」という問いがよく出ます。ただ、私はこの問いの立て方に違和感があります。安全かどうかを二択で答えられる技術は、おそらく存在しないからです。
この記事で書きたい主張は次の一文です。
脆弱性が発生しない技術を探すのではなく、脆弱性が発見されたときに、影響範囲を把握し、修正し、検証し、説明できる技術スタックを選ぶべきである。
その観点で見たとき、C#/.NET は責任を持って開発・運用するための土台を作りやすいエコシステムだと私は考えています。ここで言いたいのは「C#/.NET なら安全」ではありません。「安全性について継続的に確認し、更新し、検証し、説明するプロセスを設計しやすい」ということです。
事実と意見は分けて書きます。公式ドキュメントで確認できた内容には出典リンクを付け、私の考えや運用提案はそう分かるように書きます。技術仕様は執筆時点(2026 年 10 月 23 日)の記述に基づいています。読者のみなさんは、お使いの SDK バージョンとプロジェクト設定で、必ず手元の挙動を確認してください。
脆弱性が見つからない技術はない
まず前提を置きます。どの層にも、脆弱性が見つかる可能性があります。
アプリケーションが動くまでには、多くの層が積み重なっています。自分たちが書いたコード、利用しているライブラリ、フレームワークとランタイム、言語とコンパイラ、ビルドを実行する環境、その下の OS やコンテナイメージ、デプロイ基盤、そして運用プロセスです。このどこかに問題が見つかれば、自分たちのシステムが影響を受けます。
しかも、問題はコードの外側にもあります。更新の適用が遅れること、使っているバージョンのサポートが終わっていること、どの依存関係を使っているのか把握できていないこと。こうした運用上の状態も、脆弱性と同じくらい現実のリスクになります。
C#/.NET も例外ではありません。.NET ランタイムにも、ASP.NET Core にも、NuGet で配布されるパッケージにも、既知の脆弱性が報告され得ます。パッケージの更新が止まることがあります。推移的依存(依存パッケージがさらに依存しているパッケージ)に問題が潜むこともあります。サプライチェーンのリスクもあります。サポートが終了した .NET のバージョンでは、セキュリティ更新を受けられません。公式ドキュメントにもその旨が書かれています(Releases and support for .NET)。
だから、問いを次のように置き換えたいのです。
脆弱性が少ない技術はどれか、ではない。脆弱性が見つかったとき、自分たちはその技術に責任を持って対応できるだろうか。
「少ない」「多い」は、測り方で結論が変わります。一方で「見つかったときに対応できるか」は、自分たちの体制と、使っている技術のエコシステムが提供する仕組みの両方を見れば、ある程度具体的に検討できます。以降はこの問いで話を進めます。
AI時代には実装と責任が分離する
生成 AI は、脆弱性対応の多くの作業を支援してくれます。影響しそうなコード箇所の調査、依存パッケージの更新、非推奨 API の置き換え、修正案の作成を任せられます。テストコードの生成、プルリクエストの作成、変更内容の説明文の下書きも同様です。
どれも、以前は人間が時間をかけていた作業です。私自身、依存関係の更新に伴う細かい修正は、AI に下書きを任せることが増えました。
ここで注意したいのは、修正案を生成することと、リリースしてよいと判断することは別の仕事だという点です。AI は差分を作れます。しかし、その差分が本当に問題を解消しているのか、副作用はないのか、業務上許容できるのかは、差分を眺めるだけでは決まりません。
人間に残る判断は、たとえば次のようなものです。
- 自分たちのシステムは、その脆弱性の影響を本当に受けるのか
- 更新に伴う互換性の問題を許容できるか
- 業務上、変えてはいけない振る舞いはないか
- 対応後に残るリスクは何か
- 緊急リリースを行うべきか、次回の定期リリースに回してよいか
- 修正までの間、一時的な緩和策を取るか
- 顧客や関係者に何をどう説明するか
最後の「説明」は特に重要です。AI が提案した変更であっても、リリースした後に説明を求められるのは人間です。「AI がそう提案したから」は、説明になりません。
私は、このことを次の一文にまとめています。
AIに任せるのは作業であって、責任ではない。
そうだとすると、技術スタックに求める条件も変わります。AI がいくら速く変更を作れても、その変更を既存のビルド・テスト・レビューの流れに戻し、人間が検証して受け入れられなければ、責任を取れる状態にはなりません。つまり、AI 時代の技術選定では「AI が書きやすいか」よりも、「AI が作った変更を人間が検証しやすいか」が重要になると考えています。
そこでC#/.NETを考える
ここで、C#/.NET を一つのエコシステムとして見てみます。
具体的には、.NET ランタイム、.NET SDK、ASP.NET Core、標準ライブラリ、NuGet、MSBuild、Visual Studio と Visual Studio Code、GitHub Actions や Azure DevOps といった CI/CD、サポートポリシー、そして公式ドキュメントです。これらが互いにつながっていて、同じ語彙とコマンドで扱えることが、私にとっては大きな意味を持ちます。
誤解がないように書いておきます。「全部 Microsoft 製だから安全」と言いたいわけではありません。そのような理屈は成り立ちません。NuGet には Microsoft 以外の多くの作者が公開したパッケージがありますし、そこにも脆弱性は含まれ得ます。
私が評価しているのは、誰が、どの範囲を、どのライフサイクルで保守しているのかを確認しやすいことです。ランタイムやフレームワークについては、サポート期間が公式に定義されています。パッケージについては、依存関係と既知の脆弱性を調べる手段があります。ビルドについては、設定をリポジトリに置けます。
ランタイムそのものへの信頼について、私が重く見ている事実もあります。.NET は、Azure、Xbox、Microsoft 365 といった Microsoft 自身の大規模サービスで使われています。たとえば Azure Active Directory(現 Microsoft Entra ID)のゲートウェイは、54 の Azure データセンターで 1 日あたり約 1,850 億件のリクエストを処理しており、.NET 6 で動作していると公開されています(Azure Active Directory's gateway is on .NET 6.0!)。Microsoft Teams の基盤サービス群の .NET 6 への移行や、Xbox のサービス開発での .NET Aspire の活用も、.NET Blog で紹介されています(Microsoft Teams' Infrastructure and Azure Communication Services' Journey to .NET 6、Xbox + .NET Aspire)。
こうした環境は、通常のアプリケーションではまず経験しない負荷や、多様な入力にさらされます。同じゲートウェイの .NET Core 3.1 への移行記事では、テスト環境や DogFood 環境に段階的に展開してバグを早期に見つけたと書かれています。また、大量のデータを扱う中で見つかった FileBufferingReadStream の性能上のボトルネックや、HTTP ヘッダーの文字コードの扱いが、ASP.NET Core や .NET 側の修正につながった経緯も、GitHub の Issue や Pull Request へのリンク付きで記録されています(Azure Active Directory's gateway service is on .NET Core 3.1!)。
ここからは私の受け止め方です。大規模でシビアな環境でしか表面化しない問題が見つかり、それが公開リポジトリでランタイムやフレームワークの修正として戻ってくる。この循環が見えることは、私にとってランタイムへの大きな信頼につながっています。ただし、それは「だから自分たちのアプリケーションも安全だ」という意味ではありません。ランタイムが鍛えられていることと、その上に自分たちが書いたコードや選んだパッケージの責任は、別の話です。
脆弱性が見つかった後の作業は、突き詰めると「調べる」「更新する」「検証する」の三つです。この三つの境界を整理しやすいことが、このエコシステムの価値だと私は考えています。次の章から、評価したい点を六つに分けて書きます。
C#/.NETを評価したい六つの理由
ランタイム、SDK、主要フレームワークの関係を追いやすい
脆弱性の情報を見たとき、最初に知りたいのは「自分たちのどれが対象か」です。.NET の場合、確認する対象は次のように整理できます。自分たちが対象にしているバージョンと、そのサポート状況です。修正版の有無、更新が必要なのが SDK かランタイムかその両方か、再ビルドが必要かも確認します。ホスティング環境の側で更新が必要かどうかも調べます。
公式ドキュメントでは、リリースの種類とサポートの考え方が整理されています。修正を含む更新(サービシングアップデート、servicing update)はほぼ毎月提供されます。セキュリティ修正を含む場合は、毎月第 2 火曜日の Patch Tuesday にリリースされると説明されています。SDK は、ランタイムとは異なるバージョン体系を持ちます。たとえば最初の .NET 9 SDK は 9.0.100 で、百の位ごとの区切りをフィーチャーバンドと呼びます(.NET releases, patches, and support)。
また、ランタイムのパッチバージョンは、アプリケーションが既定でインストール済みの最新パッチを使うと説明されています。セキュリティ修正をすぐ使うための既定動作です(同ページ)。
もちろん、これで全部が自動的に解決するわけではありません。自己完結型の配置か、フレームワーク依存の配置か、コンテナか、マネージドなホスティングかによって、更新の責任範囲は変わります。それでも、調べるべき境界の名前が揃っていることは、調査の出発点として大きいと感じます。次は、その調査のうち依存パッケージの部分です。
NuGetの依存関係と既知の脆弱性を確認できる
NuGet には、パッケージの既知の脆弱性を報告する NuGet Audit があります。公式ドキュメントによれば、restore のたびに、各プロジェクトが使うパッケージをサーバーの VulnerabilityInfo リソースと照らし合わせます(Auditing package dependencies for security vulnerabilities)。
どの脆弱性情報を参照するかは、NuGet.Config の auditSources で指定できます。未指定の場合は packageSources が使われます。nuget.org の脆弱性データベースの情報源は GitHub Advisory Database だと明記されています。ここで言えるのは、公式ドキュメントに書かれた範囲のことだけです。個々の脆弱性データベースの網羅性や鮮度については、このドキュメントからは判断できません。
コマンドラインで確認するには、dotnet package list が使えます。これは .NET 10 で導入された「名詞が先」の書式で、.NET 9 SDK 以前では従来の dotnet list package を使うと説明されています(dotnet package list)。
# .NET 10 SDK 以降
dotnet package list --vulnerable --include-transitive
# .NET 9 SDK 以前
dotnet list package --vulnerable --include-transitive
--vulnerable は既知の脆弱性があるパッケージを一覧し、--include-transitive で推移的依存も含めます。NuGet のドキュメントにも、--include-transitive は既定ではないので指定が必要だと注記されています。--vulnerable は --deprecated や --outdated とは同時に指定できません。また、--vulnerable が AuditSources を使うのは .NET SDK 9.0.300 以降だと書かれています。
機械的に処理したいときは、--format json で JSON 形式のレポートを出せます。公式のコマンド例にも、推移的依存と脆弱性の詳細を JSON ファイルへ保存する例が載っています。レポートを CI の成果物として残せば、後から「その時点で何が見えていたか」を説明する材料になります。
脆弱性監査をリポジトリとCIの方針にできる
NuGet Audit の設定は MSBuild プロパティで表現できます。ドキュメントによれば、主なプロパティと既定値は次のとおりです。
| プロパティ | 既定値 | 取り得る値 |
|---|---|---|
NuGetAudit |
true |
true / false
|
NuGetAuditMode |
下記参照 |
direct / all
|
NuGetAuditLevel |
low |
low / moderate / high / critical
|
NuGetAuditMode の既定値は、net10.0 以上を対象とするプロジェクトでは all、それ以外では direct です。複数のターゲットフレームワークを指定した場合、どれか一つでも既定が all なら、すべてに all が使われると書かれています。つまり、対象バージョンによって既定の監査範囲が変わります。これらの既定値は SDK と NuGet のバージョンに依存します。読者のみなさんの環境で確認してください。
リポジトリ全体に方針を適用するには、Directory.Build.props に書くのが一つの方法です。
<Project>
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
</PropertyGroup>
</Project>
この設定では、推移的依存も含めて監査し、重大度が moderate 以上の問題を報告します。公式ドキュメントも、これらのプロパティをプロジェクト単位でもリポジトリ単位でも定義できると説明しています。
ビルドとの関係も整理しておきます。重大度ごとに警告コードが分かれていて、NU1901 が low、NU1902 が moderate、NU1903 が high、NU1904 が critical です。NU1905 は、監査ソースが脆弱性データベースを提供していないことを表します。これらは警告として出るため、通常の警告の扱いと同じように、エラーに昇格できます。ドキュメントには、high と critical だけをエラーにする例として次の書き方が載っています。
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>
逆に、TreatWarningsAsErrors を使っていると、将来見つかった脆弱性でビルドが突然失敗することがあります。これを避けたい場合は、WarningsNotAsErrors に NU1901 から NU1904 を加える方法も紹介されています。ここは、組織としてどちらの考え方を取るかを決める部分です。
特定の脆弱性情報を報告から外したいときは、NuGetAuditSuppress を使います。
<ItemGroup>
<NuGetAuditSuppress Include="https://github.com/advisories/XXXX" />
</ItemGroup>
XXXX は実際の勧告 URL に置き換えます。公式ドキュメントでも、影響を分析して該当しない、またはリスクを受け入れられると判断した場合の、最後の手段(last resort)と位置付けられています。
ここまでは公式ドキュメントの内容です。以下からは、私の運用提案です。
- 脆弱性の検出を一度きりではなく、継続的に行う(CI で定期的に restore と監査を実行する)
- どの重大度から通知するかを、組織で決める
- どの条件でビルドを失敗させるかを、組織で決める
- 抑制する場合は、理由と期限を記録する
- 依存関係を更新した後は、再監査と回帰テストを行う
大事なのは、これらが「誰かの注意力」に依存しないことです。監査の対象と重大度のしきい値をリポジトリの設定として表現できれば、担当者が変わっても、レビューで設定の変更を確認できます。ただし、これは「安全の保証」ではありません。NuGet Audit が報告できるのは、既知の脆弱性情報として登録されたものだけです。未知の問題や、自分たちのコードの欠陥は、別の手段で見つける必要があります。
LTSとサポートポリシーを技術選定に含められる
脆弱性対応では、「更新を受け取れる状態にあるか」が前提になります。.NET では、サポートポリシーが公開されており、技術選定の判断材料に含められます。
公式ドキュメントによれば、2026 年 10 月 23 日時点でサポート中のバージョンは次のとおりです。
| バージョン | 種別 | サポート終了日 |
|---|---|---|
| .NET 10 | LTS(Long Term Support) | 2028 年 11 月 14 日 |
| .NET 9 | STS(Standard Term Support) | 2026 年 11 月 10 日 |
| .NET 8 | LTS | 2026 年 11 月 10 日 |
最新の一覧と終了日は、.NET サポートポリシーで確認してください。執筆時点では、.NET 8 と .NET 9 のサポート終了が翌月に迫っています。この記事を読んだタイミングによっては、状況が変わっている可能性があります。
私が重視しているのは、サポート終了が「管理可能な期限」として計画に入れられる点です。ドキュメントによれば、STS のサポート期間は 24 か月です。LTS は最低 3 年、または次の LTS のリリースから 1 年後のいずれか遅い日までです(.NET releases, patches, and support)。移行先と移行時期を、事前にスケジュールとして置けます。
もう一つ、重要な条件があります。サポートポリシーは、サポートを受けるには最新のパッチ更新を適用していることが前提だと説明しています。サポート対象のバージョンを使っていても、パッチを当てていなければ、サポートされた状態ではありません。
AI を使うときは、この点にも気をつけたいところです。AI は、古いサンプルコードや、非推奨になった API、すでにサポートが終了した構成を提案することがあります。提案された構成が、今サポートされているものかどうかは、人間が公式の情報で確認する必要があります。
標準機能で作れる範囲が広い
.NET には、Web アプリケーションの基盤である ASP.NET Core をはじめ、依存性の注入、構成管理、ログ、HTTP クライアント、JSON の処理、認証と認可、暗号化の API が、標準の機能として用意されています。
外部パッケージを否定する理由にしたいわけではありません。外部パッケージには、それぞれ固有の機能や価値があります。採用するなら、「そのパッケージならではの機能は何か」「自分たちの要件に対して、なぜそれが必要だったのか」を説明できるようにしておきたいと考えています。ただ、パッケージを一つ追加するたびに、管理の対象も増えます。
具体的には、そのパッケージが今も保守されているか、推移的依存として何が増えるかを見ます。ライセンス、脆弱性情報の発信状況、更新時の互換性、配布経路、メンテナーへの信頼も確認の対象です。
個人的には、メンテナーに知人が多く、信頼を置いているパッケージも多くあります。誰が保守しているのかを知っていることは、私にとって採用を考えるうえでの大切な背景です。ただ、その信頼と、自分たちのシステムに必要な機能かどうかは、分けて考えています。知人が保守しているからという理由だけで採用するのではなく、機能上の必要性を説明し、採用後も更新や脆弱性情報を確認します。
私の結論は単純です。標準機能で十分なら標準機能を使います。外部パッケージが適切なら採用します。ただし、採用した理由と、更新の責任者を明確にしておきます。
たとえば、パッケージの追加時に、「標準機能だけでは満たせない要件は何か」「このパッケージ固有のどの機能が必要か」「誰が更新を追うのか」を、プルリクエストの説明に書く運用が考えられます。あとから依存関係を見直すときにも、当時の採用理由が判断材料になります。これは .NET 固有の話ではありません。ただ、標準で提供される範囲が広いほど、この判断の対象は少なくなります。なお、ここで書いた標準機能の範囲の広さは、各機能の公式ドキュメントで読者自身がご確認ください。
AIが生成した変更を通常の開発プロセスへ戻せる
最後に、一番言いたい点です。AI が生成した変更を、通常の開発プロセスへ戻せることです。
脆弱性対応を、あくまで一例として次のような流れで考えてみます。
脆弱性情報を検出する
↓
影響するプロジェクトと依存関係を特定する
↓
AI が更新案と修正案を作成する
↓
dotnet restore
↓
dotnet build
↓
dotnet test
↓
NuGet Audit で再確認する
↓
人間が差分と業務影響をレビューする
↓
展開を判断する
↓
結果と残存リスクを記録する
これが唯一の正解だとは考えていません。チームの状況によって、順序も手順も変わります。重要なのは、AI の作業が流れの中央にあるだけで、前後を既存のコマンドとレビューが固めていることです。
dotnet restore、dotnet build、dotnet test は、AI が使うときも人間が使うときも同じです。AI が書き換えたコードが本当にビルドできるのか、テストが通るのか、更新後に脆弱性の報告が消えたのかは、同じコマンドで確かめられます。この「同じ手続きで確かめられる」ことが、AI の出力に対する信頼の足場になると私は考えています。
そして、最終的に受け入れるのは人間です。ビルドとテストが通っても、それは「受け入れてよい」の十分条件ではありません。テストに書かれていない業務上の振る舞いが変わっていないかは、レビューする人が読み取る必要があります。
「C#はいいぞ」の意味
タイトルにある「あえて」は、流行や勢いではなく、責任の観点で選ぶという意味で使っています。そのうえで、私が「C#/.NET はいいぞ」と言うときの中身を、あらためて書いておきます。
.NETランタイム、ASP.NET Core、NuGet、SDK、ビルド、テスト、監査、サポートポリシーまで含めて、開発から脆弱性対応までを一続きのプロセスとして設計しやすい。
AIが変更を生成しても、その変更を通常の開発プロセスへ戻し、人間が確認して受け入れられる。
その管理可能性を含めて、C#/.NETはいいぞ、と言いたい。
ここで言う「設計しやすい」は、勝手にそうなるという意味ではありません。監査の設定を書き、CI に組み込み、抑制のルールを決め、サポート期限を追う作業は、人間が行います。エコシステムが用意しているのは、その作業の足場です。
この記事で紹介した設定やコマンドも、導入しただけで安全になるものではありません。
結論
脆弱性が見つからない技術を探すことは、現実的ではないと私は考えています。大切なのは、見つかったときに対応できる状態を、あらかじめ作っておくことです。
具体的には、何を使っているかを把握します。影響範囲を確認します。修正版へ更新します。ビルドとテストで検証します。残るリスクを理解します。そして最終的な判断と説明責任は、人間が持ちます。
C#/.NET には、このプロセスを支える土台があります。公式に定義されたサポートポリシー、依存関係と既知の脆弱性を確認できる NuGet Audit、リポジトリ単位で方針を表現できる MSBuild、通常の開発フローに戻せるビルドとテストです。ただし、土台は土台であって、保証ではありません。サポート切れのバージョンを使い続ければ更新は受けられませんし、NuGet Audit が報告できるのは既知の情報に限られます。
AIに任せるのは作業であって、責任ではありません。その責任を引き受けるための技術スタックとして、C#/.NET はいいぞ、と私は考えています。
参考資料
- Releases and support for .NET
- .NET Support Policy
- Auditing package dependencies for security vulnerabilities
- dotnet package list
- Azure Active Directory's gateway is on .NET 6.0!
- Azure Active Directory's gateway service is on .NET Core 3.1!
- Microsoft Teams' Infrastructure and Azure Communication Services' Journey to .NET 6
- Xbox + .NET Aspire: Transforming Local Development Practices