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

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

更新履歴(4件・最終更新 2026年08月02日)

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

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(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://staging.comcomponent.com/blog/2026/03/14/001-windows-app-security-minimum-checklist/

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

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

このファイルの中身は、4 章のリリース前チェックリスト(8 カテゴリ 32 項目)と同じです。違うのは、StatusNotes の記入欄が付いていることと、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. まず結論

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

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

この記事の知識マップ

この記事はWPF/WinForms/WinUI/C++/C#のWindows業務アプリを対象に、リリース前に外したくない最低限のセキュリティ対策をチェックリスト化しています。中心は、アプリ全体をrequireAdministratorにせず必要な処理だけを別プロセスやサービスへ分離すること、配布物にコード署名とタイムスタンプを付けて未署名配布や証明書検証の常時スキップを避けること、秘密情報を平文設定に置かずDPAPIなどで保護することです。あわせて、SQLの文字列連結が招くSQLインジェクションをプレースホルダで防ぐこと、DLLを名前だけで読み込むと検索順序ハイジャックを招くこと、ログへ機密情報を出すとログ自体が漏えい経路になることも扱っています。

Windowsアプリセキュリティ最低限チェックリストの知識マップ管理者権限の扱い、コード署名と更新物の検証、秘密情報の保護、SQLインジェクションやDLL読み込みなどの入力・実行時リスク、ログでの機密漏えいという最低限のセキュリティ項目どうしの関係を示す図で構成できるで構成できる用いるのは非推奨推奨される対応原因になり得る原因になり得る防止する軽減する防止する推奨される対応推奨される対応前提とする用いるのは非推奨原因になり得る防止する原因になり得る推奨される対応前提とする利用する前提とする推奨される対応管理者権限コード署名証明書アプリの秘密情報asInvoker実行レベルrequireAdministrator実行レベル対話ユーザーのデスクトップアプリ管理者権限処理の分離DLL検索順序ハイジャックDLLの名前だけでの読み込み未署名バイナリコード署名のタイムスタンプ証明書の期限切れ更新物の署名・ハッシュ検証未検証の更新適用証明書のピン留め証明書検証の常時スキップ証明書の失効確認SQL文字列連結SQLインジェクションプレースホルダ(プリペアドステートメント)ログへの機密情報出力ログ経由の機密情報露出DPAPI秘密情報の平文保存Windowsサービス

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

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

2.1. 対象にする範囲

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

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

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

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

2.2. 対象外

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

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

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

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 すり替え、設定ファイル誤読、外部入力の不備が、そのまま強い権限で実行されます。

基本方針はこうです。

  • 通常の 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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • ソースコードに直書きした API キー
  • appsettings.jsonapp.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 はユーザー プロファイルに鍵を持つので、プロファイルが読み込まれていない状態(impersonation 中など)では復号に失敗することがドキュメントに明記されています。サービスからの利用を考えるなら、この点も確認しておいてください。

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 まわりの設定が要るなら SocketsHttpHandlerHttpClientHandler 側に持たせるほうが素直です。

ただし、「古い API だからもう効かないだろう」と放置するのは危険です。Microsoft のドキュメントには、ServicePointManager.ServerCertificateValidationCallback.NET 9 以降は SocketsHttpHandler.SslOptionsRemoteCertificateValidationCallback にマップされる と書かれています。つまり、どこか 1 行の => true が、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 は、対象証明書のフィンガープリントを定数か設定から与えます。

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

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

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 が普通に入ってきます。

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 に入っていたりすると、静かに壊れます。 セキュリティだけでなく、障害予防としてもかなり効きます。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

コード検索は 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 件だった」まで残しておくと、次のリリースでは差分だけを見れば済みます。

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 読み込み、ファイル保存先でだいたい苦しくなります。 最小権限のほうが長期では安定します。

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 番の署名と同じ優先度で見てください。更新モジュールは、本体より弱いのに本体を書き換えられる立場にあります。

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

7. まとめ

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

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

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

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

8. 参考資料

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

Windows I/Oの深層(第6回・最終回) ── フィルタードライバーとミニフィルター:ProcmonとウイルススキャンがI/Oに割り込める理由

Windowsのフィルタードライバーとミニフィルターを図解で解説する連載の最終回です。フィルターマネージャーとアルティチュード、pre/postコールバック、Procmonやウイルス対策が全I/Oを検査できる仕組み、「あの環境だけ遅い」の調査手順まで整理します。

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

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

よくある質問

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

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

著者プロフィール

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

小村 豪

合同会社小村ソフト 代表

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

ブログ一覧に戻る