Windowsアプリ開発のセキュリティ最低限チェックリスト

· 更新日: · · Windows開発, セキュリティ, 設計, C# / .NET, Win32

更新履歴(5件・最終更新 2026年08月23日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

権限分離の構図、署名とタイムスタンプ、更新経路の検証、DPAPIの往復、証明書ピン留めの判定の流れなどを図でも追えるように、Mermaid図を21点追加しました(地の文500〜750字につき1図の規約に合わせたものです)。本文の文章は変えていません。
記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
証明書をピン留めする例で、サムプリントが一致したら`SslPolicyErrors`の内容にかかわらず`true`を返していたのを直しました。この書き方では、ピン留めした証明書が期限切れでも、鍵が漏れて失効させたあとでも通り続けます。認めるのは`RemoteCertificateChainErrors`だけにし、チェーンの状態も`UntrustedRoot`と`PartialChain`(社内CAが端末に入っていない)以外は拒否するようにしました。`CheckCertificateRevocationList = true`を追加し、失効の確認手段が無い場合にどう埋めるかも本文に書いています。
各項目の確認方法の節を新設しました(マニフェストの確認、`Get-AuthenticodeSignature`、秘密情報やSQL連結の検索語、Process Monitorのフィルター条件、`dotnet list package --vulnerable`)。DPAPIの例を保存と読み込みの往復に差し替え、entropyの一致要件と`CurrentUser`と`LocalMachine`の違いを追記しました。.NET 9以降は`ServicePointManager`のコールバックが`SocketsHttpHandler`にマップされる点を根拠に、ハンドラー単位で対象を限定する例を追加しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589635)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Windowsアプリ開発のセキュリティ最低限チェックリスト」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589635 https://comcomponent.com/blog/2026/03/14/001-windows-app-security-minimum-checklist/

DOI(最新版)
10.5281/zenodo.21589635
DOI(この版)
10.5281/zenodo.22064158

Excel 版チェックリストをダウンロード

このファイルの中身は、4 章のリリース前チェックリスト(8 カテゴリ 32 項目)と同じです。違うのは、Status と Notes の記入欄が付いていることと、Checklist-ja / Checklist-en の 2 シートで日本語と英語の両方が入っていることだけです。読みながら確認するなら 4 章、レビュー記録として配るなら Excel 版、という使い分けで足ります。

Windows アプリのセキュリティというと、話が急に大きくなりやすいです。 ゼロトラスト、EDR、SBOM(Software Bill of Materials、ソフトウェア部品表)、証明書運用、脆弱性管理。どれも大事ですが、実務ではその前に外したくない基本がいくつもあります。

特に、次のようなアプリでは「高度な防御」より先に、基本の漏れを塞ぐほうが効きます。

  • WPF / WinForms / WinUI のデスクトップアプリ
  • C++ / C# の Win32 アプリ
  • 装置連携、ファイル連携、DB 接続、社内配布ツール
  • 自動更新機構を持つ業務アプリ
  • Windows サービスや補助 EXE を含む構成

Windows アプリ開発では、全部を一気に完璧にするより、まず明らかに危ない穴を残さないほうが現実的です。 ここでは、設計、実装、配布、運用の順に、最低限外したくないポイントをチェックしやすい形で整理します。

この記事の進め方高度な防御より先に基本の漏れを塞ぐという方針で、設計・実装・配布・運用の順に最低限のポイントを整理するという記事の流れを示す図。その前に高度な防御(ゼロトラストなど)基本の漏れを塞ぐ設計実装配布運用

図1: 高度な防御の前に基本の漏れを塞ぐ方針で、設計・実装・配布・運用の順に見ていく。

1. まず結論

  • 最初に外したくないのは、不要な管理者権限を要求しないこと、署名すること、秘密情報を平文で持たないこと、証明書検証を無効化しないことです。
  • Windows アプリは、配布物そのものが攻撃面になります。EXE / DLL / MSI / MSIX / 自動更新モジュールまで含めて見るほうが安全です。
  • ServerCertificateValidationCallback => true、平文の接続文字列、LoadLibrary("foo.dll") の雑な読み込み、文字列連結での SQL 実行は、最低限のラインでも避けたい項目です。
  • 管理者権限が必要な処理が一部だけなら、アプリ全体を昇格させるのではなく、その部分だけを別 EXE や service に分けるほうが安全です。
  • Windows で配布するアプリは、署名 + タイムスタンプを前提に考えたほうがよいです。利用者への信頼性だけでなく、改ざん検知や運用説明もしやすくなります。
  • 保存時の機密情報は、用途に応じて DPAPI / ProtectedData や Credential Locker を使い分けます。少なくとも appsettings.json に平文で置く状態は抜けたいところです。
  • ログは多ければよいわけではありません。トークン、パスワード、接続文字列、個人情報、フルリクエスト本文をそのまま残すと、ログ自体が事故の主役になります。

最低限のセキュリティは、特殊な機能を足すことより、危ない既定動作や雑な実装を残さないことです。

最低限のセキュリティの考え方最低限のセキュリティは特殊な機能を足すことではなく、危ない既定動作や雑な実装を残さないことだという整理を示す図。最低限の主眼ではないこれが主眼特殊な機能を足す最低限のセキュリティ危ない既定動作・雑な実装を残さない

図2: 最低限のラインは機能追加ではなく、危ない既定動作や雑な実装を残さないこと。

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全21件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. この記事の対象と「最低限」の意味

2.1. 対象にする範囲

この記事で想定しているのは、こういった Windows アプリです。

  • WPF / WinForms / WinUI のデスクトップアプリ
  • C++ / C# の Win32 アプリ
  • 社内配布ツール、装置連携ツール、監視ツール
  • 補助 EXE、Windows サービス、アップデータを含む構成
  • EXE / MSI / MSIX で配布する業務用ソフト

ここでいう「最低限」は、監査に通る最終形ではなく、これが抜けていると普通に事故る項目です。

この記事での「最低限」の意味この記事の最低限は監査に通る最終形ではなく、抜けていると普通に事故る項目を指すという線引きを示す図。ここでは指さないこれを指す監査に通る最終形この記事の「最低限」抜けていると普通に事故る項目

図3: 「最低限」が指すのは監査対応の最終形ではなく、抜けると普通に事故る項目のほう。

コード例の前提もそろえておきます。C# の例は .NET 8 以降、C++ の例は Win32 API を想定しています。.NET Framework 4.8 でも考え方はそのままですが、推奨される書き方が変わっている箇所があります。代表例が 3.6 の ServicePointManager まわりで、新規コードでは IHttpClientFactory と HttpClient を使う前提に変わっています。古い世代のコードを読み直すときは、そこだけ注意してください。

2.2. 対象外

一方で、この記事の中心からは外すものもあります。

  • 企業全体のゼロトラスト設計
  • EDR / SIEM / DLP / MDM の全体運用
  • カーネルドライバの詳細なハードニング
  • 暗号設計そのものを一から行う話
  • 高度な脅威分析やフォレンジック手順

つまり、「組織全体の巨大なセキュリティ施策」ではなく、Windows アプリ開発者がリリース前に自力で外しにくい基本線を扱います。

対象と対象外の線引き組織全体の巨大なセキュリティ施策は対象外とし、Windowsアプリ開発者がリリース前に自力で外しにくい基本線を対象にするという範囲の線引きを示す図。この記事では扱わないこれを扱う組織全体の巨大な施策この記事の範囲開発者が自力で外しにくい基本線

図4: 扱うのは組織全体の施策ではなく、開発者がリリース前に自力で押さえられる基本線。

3. まず見るチェックリスト

細かい議論の前に、まず全体を見渡せる表を置きます。 ここだけでも、見直す場所の当たりは付きます。

3.1. 全体像

確認する項目 最低限やること 典型的な NG
実行権限 asInvoker を基本にし、昇格が必要な処理だけ分離する アプリ全体を requireAdministrator にする
配布物の信頼性 EXE / DLL / MSI / MSIX にコード署名し、タイムスタンプも付ける 未署名のまま配布する
更新 更新元を固定し、HTTPS と署名確認で改ざん検知する HTTP ダウンロード後にそのまま上書きする
機密情報 ソースコードや平文設定に秘密を置かず、DPAPI / Credential Locker 等を使う API キーや接続文字列を設定ファイルに平文で置く
通信 HTTPS を使い、証明書検証を無効化しない return true で証明書検証を常時スキップする
外部入力 SQL、ファイル、IPC、URI、CSV、JSON などを全部検証する 「社内ツールだから」で素通しする
DLL 読み込み 絶対パス、SetDefaultDllDirectories、安全な検索順序を使う LoadLibrary("foo.dll") を現在ディレクトリ任せにする
ログ トークン、パスワード、PII をマスクし、利用者向けエラーは出し分ける 例外詳細や接続文字列をそのまま表示、保存する
依存関係 SDK、NuGet、VC++ ランタイム、OSS 依存を継続的に更新する 数年単位で固定し、脆弱性情報も追わない

3.2. 権限は asInvoker を基本にする

Windows アプリで最初に見直したいのはここです。 アプリ全体を管理者権限で動かすと、バグや DLL すり替え、設定ファイル誤読、外部入力の不備が、そのまま強い権限で実行されます。

アプリ全体を管理者権限にしたときの危険アプリ全体を管理者権限で動かすと、バグやDLLすり替え、設定ファイル誤読、外部入力の不備がそのまま強い権限で実行されることを示す図。アプリ全体を管理者権限で動かすバグDLL すり替え設定誤読・入力不備そのまま強い権限で実行される

図5: アプリ全体を昇格させると、抱えている不備のすべてが強い権限で実行されてしまう。

基本方針はこうです。

  • 通常の UI アプリは asInvoker
  • 管理者権限が必要な処理だけ別プロセスや service に分離する
  • 昇格は必要な瞬間だけ行う
  • 補助 EXE や service に渡す入力も検証する

通常は閲覧と編集だけのデスクトップアプリで、インストールやファイアウォール設定変更だけが管理者権限を必要とするなら、アプリ全体を requireAdministrator にするより、昇格が必要な部分だけ broker に寄せるほうが安全です。

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  <security>
    <requestedPrivileges>
      <requestedExecutionLevel level="asInvoker" uiAccess="false" />
    </requestedPrivileges>
  </security>
</trustInfo>

「管理者で動けば楽」は、だいたい後で効いてきます。 最小権限で動かして、それでも必要な操作だけ切り出したほうが、事故の半径はかなり小さくなります。

昇格が必要な処理だけを分離する構図通常のUIアプリはasInvokerで動かし、管理者権限が必要な処理だけを別プロセスやserviceのbrokerに寄せて、昇格は必要な瞬間だけ行う構図を示す図。必要な瞬間だけ依頼UI アプリ(asInvoker)broker(別 EXE / service)昇格が必要な処理だけ実行broker へ渡す入力も検証する

図6: 通常はasInvokerで動かし、昇格が要る処理だけをbrokerに寄せると事故の半径が小さくなる。

3.3. バイナリとインストーラに署名する

Windows では、配布物の信頼性が物を言います。 ユーザーが触るのはソースコードではなく、EXE、DLL、MSI、MSIX、アップデータです。ここが未署名だと、運用上の説明も、改ざん検知も、配布時の安心感も弱くなります。

ユーザーが触るのは配布物ユーザーが触るのはソースコードではなくEXEやDLLやインストーラーやアップデータであり、そこが未署名だと改ざん検知や運用上の説明が弱くなることを示す図。ユーザーが触るものEXE / DLLMSI / MSIXアップデータ未署名だと改ざん検知も説明も弱い

図7: 攻撃面になるのは配布物そのもので、未署名のままだと信頼性の根拠を失う。

最低限、見ておきたいのはこのあたりです。

  • EXE / DLL / MSI / MSIX を署名する
  • インストーラだけでなく、更新に使う補助バイナリも署名する
  • タイムスタンプを付ける
  • 証明書の期限と更新手順を release 手順に含める

特にタイムスタンプを付けていない署名は、証明書期限切れ後の検証で困りやすいです。 「署名してあるから終わり」ではなく、署名 + タイムスタンプまでを release 手順に入れておくほうが安定します。

署名とタイムスタンプタイムスタンプを付けていない署名は証明書期限切れ後の検証で困りやすく、署名とタイムスタンプまでをrelease手順に入れると安定することを示す図。署名のみ証明書期限切れ後の検証で困りやすい署名 + タイムスタンプ期限切れ後の検証で困りにくいrelease 手順に組み込んでおく

図8: 署名だけで終えず、タイムスタンプまで含めてrelease手順に入れておく。

MSIX を使うなら、パッケージ署名は前提です。 MSI / EXE 配布でも、少なくともインストーラ本体と主要な実行バイナリは署名しておいたほうがよいです。

3.4. 更新経路を固定し、改ざん検知を入れる

今どきの Windows アプリでは、初回インストールより更新経路のほうが長く使われます。 ここが雑だと、せっかく本体を丁寧に作っても、アップデータが一番弱いところになります。

更新経路が一番長く使われる初回インストールより更新経路のほうが長く使われるため、更新まわりが雑だとアップデータがアプリ全体で一番弱いところになることを示す図。使われるのは一度長く使われ続ける初回インストールアプリの寿命更新経路雑だと一番弱いところになる

図9: 更新経路は初回インストールより長く使われ、雑に作るとそこが一番の弱点になる。

更新まわりで最低限考えておきたいのはこの 5 点です。

  • 更新ファイルの取得は HTTPS 前提
  • ダウンロードした更新物の署名やハッシュを検証する
  • 更新元 URL をコードや設定で無制限に差し替えられないようにする
  • 更新モジュール自身も署名する
  • ロールバックや失敗時の復旧手順を決める

MSIX + App Installer を採れるなら、更新の仕組みを OS 寄りに寄せやすいです。 一方で独自アップデータを持つなら、通信の安全性と配布物の真正性の両方を確認する必要があります。HTTPS だけでは「通信経路」は守れても、「そのファイルが本当に自分の発行物か」までは保証しません。

更新で確認する2つのこと独自アップデータではHTTPSによる通信の安全性と、ダウンロードした更新物の署名やハッシュによる真正性の両方を確認する必要があることを示す図。更新ファイルを HTTPS で取得署名やハッシュを検証検証を通ったものだけ適用HTTPS が守るのは通信経路だけ自分の発行物かどうかを確かめる

図10: HTTPSで取得しても真正性は別問題で、署名やハッシュの検証を通ってから適用する。

3.5. 秘密情報をソースコードや平文設定に置かない

ここは実務だと本当に事故りやすいところです。 「社内ツールだから」「どうせ exe を配るだけだから」で、接続文字列、API キー、共有フォルダ資格情報、固定トークンをソースコードや設定ファイルに置きがちです。

最低限、こういう置き方は避けたいところです。

  • ソースコードに直書きした API キー
  • appsettings.json や app.config の平文パスワード
  • リポジトリに入った接続文字列
  • 復号キーと暗号文を同じ場所に置く設計
  • 利用者ごとではなく全員共通の固定資格情報

Windows アプリで現実的に採れる選択肢は、だいたいこの 4 つです。

  • Windows の資格情報を保存したい packaged desktop app / WinUI 系なら Credential Locker を検討する
  • ローカルに秘密を暗号化保存したい Win32 / .NET なら DPAPI / ProtectedData を使う
  • 接続先が Windows 認証や統合認証を使える 可能ならアプリにパスワードを持たせない
  • クラウドやサーバー側で秘密管理できる クライアントに長期秘密を埋め込まない設計を優先する

C# なら、少なくとも次のように DPAPI を使うだけでも、平文保存よりはかなりましです。保存と読み込みの往復まで書くと、こうなります。

// C# / .NET 8。ProtectedData は Windows 専用で、
// .NET では NuGet パッケージ System.Security.Cryptography.ProtectedData が必要です。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public static class SecretStore
{
    // 復号時にも同じ値が要る。null でも動くが、付けたほうが安全。
    private static readonly byte[] Entropy = [0x4b, 0x53, 0x2d, 0x76, 0x31];

    public static void Save(string path, string secretText)
    {
        byte[] plaintext = Encoding.UTF8.GetBytes(secretText);

        byte[] ciphertext = ProtectedData.Protect(
            plaintext,
            Entropy,
            DataProtectionScope.CurrentUser);

        // バイト列のままファイルへ置いてもよいが、設定ファイルへ入れるなら Base64 にする。
        File.WriteAllText(path, Convert.ToBase64String(ciphertext));

        CryptographicOperations.ZeroMemory(plaintext);
    }

    public static string Load(string path)
    {
        byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));

        // 保存したときと同じユーザー、同じ entropy でなければ CryptographicException になる。
        byte[] plaintext = ProtectedData.Unprotect(
            ciphertext,
            Entropy,
            DataProtectionScope.CurrentUser);

        try
        {
            return Encoding.UTF8.GetString(plaintext);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(plaintext);
        }
    }
}

呼び出し側は、こうなります。

string path = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
    "SampleApp",
    "token.dat");

Directory.CreateDirectory(Path.GetDirectoryName(path)!);

SecretStore.Save(path, "example-api-key");
string restored = SecretStore.Load(path);

ここで大事なのは、「暗号化したから安全」ではなく、誰が復号できるかを設計で決めることです。 CurrentUser にするのか、LocalMachine にするのかで意味がかなり変わります。CurrentUser は保存したユーザーだけが復号でき、LocalMachine は同じマシンの誰でも復号できます。サービスとして別アカウントで動かす場合や、ユーザーを切り替える運用がある場合は、ここを先に決めておかないと、後で「読めない」「読めてしまう」のどちらかで詰まります。

DPAPIで誰が復号できるかDPAPIのスコープをCurrentUserにすると保存したユーザーだけが復号でき、LocalMachineにすると同じマシンの誰でも復号できるため、誰が復号できるかを設計で先に決める必要があることを示す図。CurrentUserLocalMachine誰が復号できるかを設計で決める保存ユーザーだけ復号可同一マシンの誰でも復号可決めないと後で詰まる

図11: DPAPIはスコープの選び方で復号できる範囲が変わるため、先に「誰が復号できるか」を決める。

なお、DPAPI はユーザー プロファイルに鍵を持つので、プロファイルが読み込まれていない状態(impersonation 中など)では復号に失敗することがドキュメントに明記されています。サービスからの利用を考えるなら、この点も確認しておいてください。

DPAPIの鍵とユーザープロファイルDPAPIはユーザープロファイルに鍵を持つため、impersonation中などプロファイルが読み込まれていない状態では復号に失敗することを示す図。プロファイル未読み込み(impersonation 中など)DPAPI の鍵ユーザー プロファイルにある復号に失敗するサービスからの利用では要確認

図12: DPAPIの鍵はユーザープロファイルにあり、プロファイルが読み込まれていない状態では復号できない。

SQL Server 接続なら、オンプレミス環境では Windows 認証を第一候補にできることがあります。 どうしても接続文字列に資格情報を含めるなら、少なくとも Persist Security Info=False を維持し、平文設定ファイルへ置きっぱなしにしないほうが安全です。

3.6. 通信は HTTPS 前提、証明書検証を殺さない

開発中だけのつもりで入れた抜け道が、そのまま本番に残る。 通信まわりの事故は、だいたいこのパターンです。

特に出荷物に残りやすいのが、この手のコードや設定です。

  • ServicePointManager.ServerCertificateValidationCallback += ... => true
  • HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  • 証明書失効確認を無効化したまま出荷
  • 開発用の自己署名証明書前提のコードを本番に残す

最低限の方針は単純です。

  • 本番通信は HTTPS
  • 証明書検証を常時スキップしない
  • 例外的な検証緩和が必要なら、対象ホストと証明書を限定する
  • 開発用の回避コードはビルド条件や設定で確実に排除する
  • .NET なら失効確認も意識する

ダメな例は、だいたいこうです。

ServicePointManager.ServerCertificateValidationCallback +=
    (_, _, _, _) => true;

一見楽ですが、これは「この HTTPS 通信は誰に繋いでも通す」に近い動きになります。 証明書検証を外すと、HTTPS を使っていても中身はかなり骨抜きです。

どの .NET を前提にしているかで書き方が変わる

ServicePointManager にグローバル設定を置くのは .NET Framework 時代の書き方です。新規コードでは、IHttpClientFactory から HttpClient を受け取り、TLS まわりの設定が要るなら SocketsHttpHandler や HttpClientHandler 側に持たせるほうが素直です。

ただし、「古い API だからもう効かないだろう」と放置するのは危険です。Microsoft のドキュメントには、ServicePointManager.ServerCertificateValidationCallback が .NET 9 以降は SocketsHttpHandler.SslOptions の RemoteCertificateValidationCallback にマップされる と書かれています。つまり、どこか 1 行の => true が、HttpClient の通信まで通してしまうことがあります。

古いコールバックが今の通信に効く経路ServicePointManagerのServerCertificateValidationCallbackは.NET 9以降SocketsHttpHandlerの検証コールバックにマップされるため、どこか1行のtrue返しがHttpClientの通信まで通してしまうことがあるという経路を示す図。.NET 9 以降マップされるServicePointManager の検証コールバックSocketsHttpHandler 側の検証HttpClient の通信にも効く1 行の true 返しが通信全体を骨抜きにし得る

図13: 古いAPIに置いた検証スキップが、マッピングを通じて今のHttpClientの通信まで通してしまう。

例外的に検証を緩めたいときは、プロセス全体に効くグローバル設定ではなく、そのハンドラーだけに閉じた形にします。

// C# / .NET 8。特定のホストと証明書だけを例外扱いする例。
// 開発用に緩めるとしても、対象を限定しないとグローバルに殺すのと変わりません。
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

// 対象証明書のフィンガープリント。設定から読んでもよい。
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";

var handler = new HttpClientHandler
{
    CheckCertificateRevocationList = true,   // 失効を見に行く。既定は false
    ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
    {
        if (errors == SslPolicyErrors.None)
        {
            return true;
        }

        // 認めるのは「この端末が社内CAを信頼していない」という一点だけ。
        // 証明書が届かない・ホスト名が一致しない、は認めない
        if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
        {
            return false;
        }

        // 相手と証明書を固定する
        if (request.RequestUri?.Host != "device.internal.example"
            || certificate is null
            || !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
                              StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        // ピン留めした 1 枚でも、期限切れと失効は認めない。
        // 認めた瞬間、「鍵が漏れたので失効させた証明書」を使い続ける経路になる
        return chain is not null
            && chain.ChainStatus.All(s =>
                   s.Status is X509ChainStatusFlags.NoError
                            or X509ChainStatusFlags.UntrustedRoot
                            or X509ChainStatusFlags.PartialChain);
    },
};

using var client = new HttpClient(handler);

ExpectedThumbprint は、対象証明書のフィンガープリントを定数か設定から与えます。

ここで大事なのは、errors と chain.ChainStatus を「見なかったことにしない」ことです。サムプリントが一致したからといって true を返してしまうと、その証明書が期限切れでも、鍵が漏れて失効させたあとでも、通り続けます。ピン留めは「この 1 枚だけを信じる」という意味であって、「この 1 枚なら何があっても信じる」ではありません。上のコードで許しているのは UntrustedRoot と PartialChain(=社内 CA がこの端末に入っていない)だけで、NotTimeValid(期限切れ)や Revoked(失効)はそのまま拒否になります。

失効を見るには CheckCertificateRevocationList = true が要ります(既定は false で、失効は確認されません)。逆に、社内 CA が CRL も OCSP も公開していないなら、RevocationStatusUnknown で弾かれます。それが正しい挙動です。失効の確認手段を用意できないなら、証明書の有効期間を短くするか、ピン留めした値を配り直せる経路を先に作っておくか、どちらかで埋めてください。「失効させられないまま長期の証明書をピン留めする」のが一番危ない状態です。

ピン留め時の判定の流れエラーなしなら通し、チェーンエラー以外は拒否し、ホスト名とサムプリントを固定し、チェーン状態でも期限切れや失効は拒否するという例外検証の判定の流れを示す図。Noneチェーンエラー以外チェーンエラーのみ不一致一致期限切れ・失効など社内CA未導入の状態だけerrors を確認通す拒否ホスト名とサムプリントを照合チェーン状態を確認

図14: ピン留めしても見なかったことにしない判定の流れで、期限切れと失効はそのまま拒否になる。

3.7. 外部入力を全部「信用しない入力」として扱う

Windows アプリは Web アプリではないので、入力 validation が甘くなりやすいです。 でも実際には、外部入力の入口は思った以上に多いものです。

  • ファイルパス
  • CSV / Excel / JSON / XML
  • コマンドライン引数
  • named pipe / socket / COM / RPC / gRPC
  • DB に渡す文字列
  • レジストリ値
  • クリップボード
  • URL / deep link
  • 外部装置や SDK から返るデータ

特に最低限外したくないのは次の 3 つです。

  1. SQL は必ずパラメータ化する 文字列連結で SQL を組まない。
  2. ファイルパスは正規化してから使う ユーザー指定パスをそのまま削除、上書き、展開に使わない。
  3. 外部ファイルの読み込みはサイズ上限と形式チェックを入れる 「開けたから安全」ではない。

SQL の例で言えば、これは避けたいです。

var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";

最低限でも、こう寄せたいです。

using System.Data;
using Microsoft.Data.SqlClient;

using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;

「社内ツールだから入力は信頼できる」は、かなり危ない前提です。 現実には、壊れた CSV、想定外のファイル名、古い DB データ、運用者の手入力ミス、他ツールが書いた中途半端な JSON が普通に入ってきます。

社内ツールにも壊れた入力は来る社内ツールでも壊れたCSVや想定外のファイル名、古いDBデータ、手入力ミス、中途半端なJSONが普通に入ってくるため、外部入力はすべて信用しない入力として扱うことを示す図。壊れた CSVアプリへの入力想定外のファイル名手入力ミス・古いデータすべて信用しない入力として検証

図15: 社内ツールでも壊れた入力は普通に来るので、入口ごとに検証してから使う。

3.8. DLL の読み込み元を曖昧にしない

これは Windows らしい落とし穴です。 LoadLibrary("foo.dll") のように名前だけで DLL を読ませると、検索順序次第で意図しない場所の DLL を拾うことがあります。

やることは決まっています。

  • 可能なら DLL の絶対パスを指定する
  • SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) を早い段階で設定する
  • AddDllDirectory で明示的に検索対象を足す
  • SearchPath の結果をそのまま LoadLibrary に渡す設計を避ける
  • safe DLL search mode に頼り切らない

たとえば native code なら、プロセス初期化の早い段階で次を入れる設計は有力です。

SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);

そして、必要な追加ディレクトリだけを AddDllDirectory で登録します。

ここは「普段は動く」ので放置されやすいのですが、配布先で作業ディレクトリが変わったり、他製品の DLL が PATH に入っていたりすると、静かに壊れます。 セキュリティだけでなく、障害予防としてもかなり効きます。

DLL読み込み元の固定名前だけでDLLを読ませると検索順序次第で意図しない場所のDLLを拾うことがあり、早い段階でSetDefaultDllDirectoriesを設定し、AddDllDirectoryで検索対象を明示し、可能なら絶対パスを指定するという対策を示す図。名前だけで読み込む検索順序次第で意図しない DLL を拾う早期に SetDefaultDllDirectoriesAddDllDirectory で明示的に追加可能なら絶対パス指定読み込み元が曖昧にならない

図16: 名前任せの読み込みをやめ、検索対象を明示して読み込み元を固定する。

3.9. ログと例外に機密を出さない

障害調査のためにログを増やすのは大事です。 ただし、ログは機密の墓場にもなりやすいです。

ログまわりで最低限見直したいのは以下です。

  • パスワード、Bearer token、API キーをログに出さない
  • 接続文字列を丸ごと出さない
  • 個人情報や業務データ本文はマスクする
  • 例外詳細は利用者向け画面と内部ログで分ける
  • debug 用の PII ログを本番で有効にしない
  • dump や trace の保存先権限を見直す

最近の .NET では redaction を前提にした整理もしやすくなっています。 少なくとも、「何でも文字列化してそのまま log」はやめたいです。

ありがちな失敗をいくつか挙げます。

  • HTTP request / response body を丸ごと保存する
  • 認証失敗時にトークンやヘッダー全体を出力する
  • 例外メッセージをそのまま MessageBox に出す
  • 保守用 ZIP に機密ログを全部同梱する

エラー表示は、たとえば次のように分けます。

  • 利用者向け: 「サーバーへの接続に失敗しました。ネットワーク設定と URL を確認してください。」
  • 内部ログ: 失敗先ホスト、TLS エラー種別、相関 ID、stack trace、再試行回数

この分離だけでも、情報漏えいと調査性のバランスがかなり良くなります。

エラー表示の出し分けエラーが起きたとき、利用者向けには簡潔な案内だけを出し、内部ログには失敗先ホストやエラー種別、相関ID、stack traceなどの調査情報を残すという出し分けを示す図。エラー発生利用者向け: 簡潔な案内だけ内部ログ: ホスト・種別・相関 ID など漏えいを防ぎつつ調査性を保つ

図17: 利用者向け表示と内部ログを分けるだけで、漏えいと調査性のバランスが良くなる。

3.10. 依存ライブラリと開発ツールを放置しない

最後は地味ですが、効果の大きい項目です。 アプリ本体を丁寧に作っても、古いランタイムや既知脆弱性のある依存ライブラリを積んだままだと、足元が抜けます。

見るべき項目自体は多くありません。

  • .NET SDK / runtime をサポート内の版に保つ
  • NuGet / OSS 依存の更新を定期的に確認する
  • C++ ならランタイム再配布物や外部 DLL の版管理をする
  • 脆弱性情報の確認を release 前チェックに入れる
  • 依存更新で壊れないように smoke test を用意する

ここは「後でまとめてやる」が一番危ないです。 半年、1年と放置すると、更新差分が大きくなりすぎて、セキュリティ対応そのものが重作業になります。

依存更新を放置した場合依存関係の更新を半年から1年放置すると更新差分が大きくなりすぎ、セキュリティ対応そのものが重作業になるという流れを示す図。これを避けられる後でまとめてやる半年〜1 年の放置更新差分が大きくなりすぎる対応そのものが重作業になる定期的に確認する

図18: 依存の更新は放置するほど差分が膨らみ、対応自体が重作業になっていく。

3.11. 各項目をどう確認するか

チェックリストは、確認方法とセットで初めて機能します。3.2 から 3.10 の項目について、リリース前に実際に叩けるものを並べておきます。

確認したいこと 確認方法
昇格を要求していないか アプリのマニフェストで requestedExecutionLevel の値を見る。ソースなら app.manifest、配布物なら Sysinternals の Sigcheck やリソース エディターで確認する
署名とタイムスタンプ PowerShell で Get-AuthenticodeSignature .\app.exe を実行し、Status が Valid か、TimeStamperCertificate が入っているかを見る。同梱 DLL と updater も 1 つずつ見る
証明書検証を殺していないか ソース全体を ServerCertificateValidationCallback、DangerousAcceptAnyServerCertificateValidator、ServerCertificateCustomValidationCallback、CheckCertificateRevocationList で検索する
秘密情報の直書き Password=、ApiKey、Secret、Token、ConnectionString で検索する。現在のソースだけでなく、リポジトリの履歴も対象にする
SQL の組み立て "SELECT、"INSERT、+ を含む文字列連結を検索し、Parameters.Add を通っているか確認する
DLL の読み込み元 Process Monitor で対象プロセスに絞り、Path ends with .dll と Result is NAME NOT FOUND のフィルターを掛ける。どこを、どの順で探しに行ったかが見えるので、意図しないフォルダーを見ていないか確認する
依存関係の既知脆弱性 dotnet list package --vulnerable --include-transitive を実行する
ログに機密が出ていないか 一度動かしてから、出力されたログを Bearer 、Password、Authorization で検索する

コード検索は rg(ripgrep)でも Visual Studio の検索でもかまいません。まとめて叩くなら、この形です。

Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
    Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
    Select-Object Path, LineNumber, Line

大事なのは、確認したこと自体を記録に残すことです。「検索した」「0 件だった」まで残しておくと、次のリリースでは差分だけを見れば済みます。

確認を記録に残す効き目リリース前の確認は結果が0件でも記録に残しておくと、次のリリースでは差分だけを見れば済むようになるという流れを示す図。リリース前に確認を実行検索した・0 件だったまで記録次のリリースは差分だけ見れば済む

図19: 「確認した」という事実まで記録しておくと、次回からは差分の確認だけで済む。

4. リリース前チェックリスト

レビューや出荷判定の雛形として、そのまま使える形にします。 表で確認しやすいよう、リリース前に最低限見たい項目をカテゴリごとに並べます。

4.1. 権限、実行方式

チェック項目 確認 メモ
通常起動は asInvoker で動く □  
管理者権限が必要な処理は別 EXE / service などに分離している □  
service を使う場合、必要以上に強い実行アカウントにしていない □  
%ProgramFiles% 配下とユーザーデータ配下の責務を分けている □  

4.2. 配布、署名

チェック項目 確認 メモ
EXE / DLL / MSI / MSIX / updater に署名している □  
署名にタイムスタンプを付けている □  
証明書の期限と更新手順を release フローに含めている □  
配布物のハッシュ確認や改ざん検知方法が決まっている □  

4.3. 更新

チェック項目 確認 メモ
更新取得は HTTPS で行う □  
ダウンロード後に署名またはハッシュを検証する □  
更新元 URL を勝手に差し替えにくい設計になっている □  
更新失敗時のロールバックまたは再試行方針がある □  

4.4. 秘密情報

チェック項目 確認 メモ
パスワード、API キー、接続文字列をソースコードへ直書きしていない □  
平文設定ファイルに秘密を置いていない □  
ローカル保存が必要な秘密は DPAPI / Credential Locker などで保護している □  
可能なところは Windows 認証やユーザー資格情報に寄せている □  

4.5. 通信

チェック項目 確認 メモ
本番通信は HTTPS を使う □  
DangerousAcceptAnyServerCertificateValidator や => true を出荷物に残していない □  
失効確認やホスト名検証を意識している □  
開発用証明書前提のコードや設定が本番に混ざっていない □  

4.6. 入力、データアクセス

チェック項目 確認 メモ
SQL はパラメータ化している □  
コマンドライン、ファイル、IPC、URI などの入力に上限と形式チェックがある □  
パス操作は正規化してルート逸脱を防いでいる □  
例外メッセージをそのまま画面へ出していない □  

4.7. DLL と実行環境

チェック項目 確認 メモ
DLL の読み込み元を明示している □  
SetDefaultDllDirectories / AddDllDirectory などで検索順序を制御している □  
カレントディレクトリや PATH 任せの DLL 読み込みをしていない □  
配布先で動的ロードに必要なファイル群を把握している □  

4.8. ログ、運用

チェック項目 確認 メモ
トークン、パスワード、PII をログに出していない □  
内部ログと利用者向けメッセージを分けている □  
dump / trace / log の保存先権限を見直している □  
SDK と依存ライブラリの更新状況を確認している □  

5. よくある NG

実務でよく見かけるのは、だいたいこんな思い込みです。

5.1. 「社内ツールだから大丈夫」

社内ツールでも、壊れたファイル、誤操作、持ち込み端末、共有フォルダ、古い DLL、雑な権限設定は普通にあります。 インターネット公開していなくても、攻撃面は消えません。

5.2. 「HTTPS だから安全」

HTTPS は大事ですが、証明書検証を無効化するとかなり意味が薄れます。 また、更新配布では HTTPS だけでなく、配布物の真正性確認も必要です。

5.3. 「暗号化したから安全」

復号キーの置き場、復号権限、ユーザー境界、マシン境界が整理されていないと、暗号化だけでは足りません。 特に LocalMachine で保護した値を「ユーザーごとの秘密」と思って使うと、後で混乱します。

5.4. 「ログを増やせば調査できる」

ログが多いだけで、トークンや個人情報が垂れ流しだと、それ自体がインシデントになります。 調査性が欲しいなら、何を残して何を伏せるかを決めるほうが先です。

5.5. 「管理者で動かせば解決」

最初は楽ですが、あとで UAC、配布、サポート、権限境界、DLL 読み込み、ファイル保存先でだいたい苦しくなります。 最小権限のほうが長期では安定します。

「管理者で動かせば解決」の末路管理者権限での常用は最初は楽でも、あとでUACや配布、サポート、権限境界、DLL読み込み、ファイル保存先で苦しくなり、最小権限のほうが長期では安定することを示す図。長期では安定管理者で動かせば解決最初は楽UAC・配布・サポートで苦しくなる権限境界・保存先で苦しくなる最小権限で動かす苦しさを避けられる

図20: 管理者常用の楽は最初だけで、長期では最小権限のほうが安定する。

6. ざっくり優先順位

全部一気にやるのが重いなら、優先順位はだいたいこうなります。

並べる基準は、事故ったときの被害の大きさと、直すコストの低さの掛け合わせです。3 章は「権限 → 配布 → 実装 → 運用」という設計の流れで並べていますが、こちらは「危ないものから」の順なので、章の順番とは一致しません。対応する節を添えておきます。

  1. 管理者権限の見直し(3.2) まず requireAdministrator の常用をやめる。被害範囲が一段変わるわりに、設計変更としては小さく済むことが多い。
  2. 署名とタイムスタンプ(3.3) 配布物の信頼性を整える。手順に組み込むだけで済み、後から入れると再配布が要る。
  3. 秘密情報の退避(3.5) ソースコード、平文設定から秘密を外す。漏れたときの被害が大きく、漏れたあとは取り返せない。
  4. HTTPS + 証明書検証の是正(3.6) => true 系を出荷物から消す。削除するだけで直ることが多く、放置すると通信が丸ごと信用できない。
  5. SQL / ファイル / IPC 入力の見直し(3.7) 文字列連結や無検証入力を減らす。件数は多いが、1 か所ずつ直せる。
  6. DLL 読み込みの固定(3.8) 名前だけロード、PATH 任せをやめる。起動処理の一部を直す作業で、障害予防にもなる。
  7. ログのマスキング(3.9) 事故時にログが二次災害にならないようにする。出力箇所が多いぶん、時間はかかる。
  8. 依存更新の定常化(3.10) リリースのたびに確認する流れにする。1 回で終わらないので、仕組みにするまでが仕事。

更新経路(3.4)がこの並びに入っていないのは、自動更新を持たないアプリもあるからです。持っているなら、2 番の署名と同じ優先度で見てください。更新モジュールは、本体より弱いのに本体を書き換えられる立場にあります。

優先順位の付け方事故ったときの被害の大きさと直すコストの低さの掛け合わせで危ないものから並べ、自動更新を持つアプリでは更新経路を署名と同じ優先度で見るという考え方を示す図。被害の大きさ掛け合わせで順位を決める直すコストの低さ危ないものから順に塞ぐ自動更新があるなら更新経路は署名と同じ優先度

図21: 優先順位は被害の大きさと直すコストの掛け合わせで決め、危ない穴から塞いでいく。

この順番なら、「まず明らかに危ない穴を塞ぐ」という意味で進めやすいです。

7. まとめ

Windows アプリ開発のセキュリティは、特別な製品や巨大な仕組みを入れる前に、 権限、署名、秘密情報、通信、入力、DLL、ログの 7 点を整えるだけでもかなり変わります。

最低限のラインを一言ずつにすると、こうなります。

  • アプリ全体を管理者権限で動かさない
  • 配布物と更新物に署名し、タイムスタンプを付ける
  • 秘密情報をソースコードや平文設定に置かない
  • HTTPS を使っても証明書検証を殺さない
  • SQL、ファイル、IPC などの外部入力を信用しない
  • DLL の読み込み元を曖昧にしない
  • ログに機密を出さない
  • 依存ライブラリを放置しない

セキュリティの話は広いですが、最初から全部をやる必要はありません。 ただ、危ない既定動作をそのまま出荷しないという最低限だけは、かなり早い段階で揃える価値があります。

8. 参考資料

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Windowsアプリのセキュリティで最初に見るべきことは何ですか?
不要な管理者権限を要求しないこと、コード署名すること、秘密情報を平文で持たないこと、証明書検証を無効化しないこと、の4点です。最低限のセキュリティは特殊な機能を足すことより、危ない既定動作や雑な実装を残さないことです。証明書検証の常時スキップ、平文の接続文字列、現在ディレクトリ任せのDLL読み込み、文字列連結でのSQL実行は最低限のラインでも避けたい項目です。
アプリ全体を管理者権限で動かしてはいけないのですか?
避けるべきです。アプリ全体を管理者権限で動かすと、バグやDLLすり替え、設定ファイル誤読、外部入力の不備が、そのまま強い権限で実行されます。通常のUIアプリはasInvokerを基本にし、管理者権限が必要な処理だけを別プロセスやserviceに分離して、昇格は必要な瞬間だけ行うのが基本方針です。
APIキーや接続文字列はどこに保存すべきですか?
ソースコードやappsettings.jsonなどの設定ファイルに平文で置く状態はまず抜け出すべきです。保存時の機密情報は、用途に応じてDPAPI / ProtectedDataやCredential Lockerを使い分けます。またログにトークン、パスワード、接続文字列、個人情報をそのまま残すと、ログ自体が事故の主役になるためマスクが必要です。
社内配布のアプリでもコード署名は必要ですか?
前提と考えたほうがよいです。Windowsアプリでは配布物そのもの(EXE / DLL / MSI / MSIX / 自動更新モジュール)が攻撃面になります。コード署名+タイムスタンプがあると、改ざん検知、利用者への信頼性、運用上の説明のしやすさが得られます。更新機構も更新元を固定し、HTTPSと署名確認で改ざんを検知できる形にすべきです。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る