Windows 應用程式開發中遵守最低限度安全性的檢核表

· · Windows 開發, 安全性, 設計, C# / .NET, Win32

Excel 版檢核表下載

講到 Windows 應用程式的安全性,話題容易突然變大。 零信任、EDR、SBOM、憑證運營、漏洞管理。每個都重要,但實務上在這之前不想漏的最低限度相當多。

特別是以下這類應用程式,比起「高階防禦」,先堵基本的漏洞更有效。

  • 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 的狀態。
  • 日誌不是越多越好。原樣留下 token、密碼、連線字串、個人資訊、完整請求本文 會讓日誌本身成為事故主角。

最低限度的安全性不是加入特殊功能,而是 不要留下危險的預設行為或粗略的實作

2. 本文對象與「最低限度」的意思

2.1. 對象範圍

本文對象是例如以下的 Windows 應用程式。

  • WPF / WinForms / WinUI 的桌面應用程式
  • C++ / C# 的 Win32 應用程式
  • 公司內部發佈工具、裝置連動工具、監視工具
  • 含輔助 EXE、Windows 服務、更新程式的構成
  • 以 EXE / MSI / MSIX 發佈的業務用軟體

這裡的「最低限度」不是 通過稽核的最終形式,而是 這個漏了相當容易出事 的項目。

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") 依賴當前目錄
日誌 遮罩 token、密碼、PII,分開使用者用錯誤的輸出 原樣顯示、儲存例外詳情或連線字串
相依關係 持續更新 SDK、NuGet、VC++ 執行環境、OSS 相依 數年為單位固定,也不追蹤漏洞資訊

3.2. 權限以 asInvoker 為基本

Windows 應用程式最先想重新檢視的是這裡。 以管理員權限執行整個應用程式,bug 或 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 應用程式比起首次安裝,更新路徑 被用的時間更長。 這裡粗略,即使本體做得精細,更新程式也會成為最弱的地方。

最低限度的思考方式如下。

  • 更新檔案的取得以 HTTPS 為前提
  • 驗證下載的更新物的 簽章或雜湊
  • 不讓更新源 URL 能以程式碼或設定無限制替換
  • 更新模組本身也簽章
  • 決定回滾或失敗時的復原步驟

若能採用 MSIX + App Installer,更新機制能靠向 OS 側。 另一方面若有自訂更新程式,需要確認 通訊安全性發佈物的真實性 兩方。光 HTTPS 只能守「通訊路徑」,不保證「那個檔案真的是自己的發行物」。

3.5. 機密資訊不放在原始碼或明文設定

這裡在實務上真的很容易出事。 以「公司內部工具所以」「反正只是發 exe」,把連線字串、API 金鑰、共享資料夾憑證、固定 token 放在原始碼或設定檔。

最低限度想避免的如下。

  • 原始碼直寫的 API 金鑰
  • appsettings.jsonapp.config 的明文密碼
  • 進入儲存庫的連線字串
  • 解密金鑰與密文放同處的設計
  • 不是每個使用者而是全員共通的固定憑證

Windows 應用程式的實務選項大致如下。

  • 想儲存 Windows 的憑證 packaged desktop app / WinUI 系可檢討 Credential Locker
  • 想在本地加密儲存機密 Win32 / .NET 可用 DPAPI / ProtectedData
  • 連線目標能用 Windows 驗證或整合驗證 盡可能不讓應用程式持有密碼
  • 能在雲或伺服器側做機密管理 優先不在用戶端埋長期機密的設計

在 C# 中至少如下用 DPAPI 也比明文儲存好得多。

using System.Security.Cryptography;
using System.Text;

byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
    plaintext,
    optionalEntropy: null,
    scope: DataProtectionScope.CurrentUser);

這裡重要的不是「加密了所以安全」,是以設計決定 誰能解密。 設為 CurrentUserLocalMachine 意義相當不同。

SQL Server 連線的話,地端環境能把 Windows 驗證作第一候補的情況有。 若無論如何需要在連線字串含憑證,至少維持 Persist Security Info=False,不要放著明文設定檔不管比較安全。

3.6. 通訊以 HTTPS 為前提,不要殺掉憑證驗證

以開發中臨時的想法放入的逃避路直接留到正式。 在通訊周邊這相當經典。

特別要注意的是以下。

  • ServicePointManager.ServerCertificateValidationCallback += ... => true
  • HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  • 停用吊銷確認就出貨
  • 以開發自簽憑證為前提的程式碼留到正式

最低限度的方針單純。

  • 正式通訊用 HTTPS
  • 不要常態跳過憑證驗證
  • 例外的驗證放寬時 限定對象主機與憑證
  • 開發用的迴避程式碼要以建置條件或設定確實排除
  • .NET 的話也意識吊銷確認

不好的例子大致如下。

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

看起來輕鬆,但這近似「這個 HTTPS 通訊連到誰都通過」。 拿掉憑證驗證,即使用 HTTPS 內容也相當被挖空。

3.7. 把外部輸入全當「不信任的輸入」處理

Windows 應用程式不是 Web 應用程式,輸入驗證容易鬆懈。 但實際上外部輸入的入口相當多。

  • 檔案路徑
  • 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
  • 驗證失敗時輸出 token 或整個標頭
  • 例外訊息原樣放到 MessageBox
  • 在維護用 ZIP 整個同捆機密日誌

錯誤顯示例如分如下。

  • 使用者用: 「連線伺服器失敗。請確認網路設定與 URL。」
  • 內部日誌: 失敗對象主機、TLS 錯誤種別、相關 ID、stack trace、重試次數

光這個分離,資訊洩漏與調查性的平衡就會變好很多。

3.10. 不要放置相依函式庫和開發工具

最後是樸素但相當有效的項目。 即使應用程式本體做得精細,載著舊的執行環境或已知漏洞的相依函式庫,腳下會垮。

最低限度想看的如下。

  • 把 .NET SDK / runtime 維持在支援內版本
  • 定期確認 NuGet / OSS 相依的更新
  • C++ 的話管理執行環境可轉散發物或外部 DLL 的版本
  • 把漏洞資訊的確認放入 release 前檢查
  • 準備 smoke test 以免相依更新壞東西

這裡「之後一起做」最危險。 半年、一年放置,更新差異太大,安全對應本身會變重工。

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 等控制搜尋順序  
沒讓 DLL 載入依賴當前目錄或 PATH  
在發佈處掌握動態載入所需的檔案群  

4.8. 日誌、運營

檢核項目 確認 備註
沒把 token、密碼、PII 放到日誌  
分開內部日誌與使用者用訊息  
重新檢視 dump / trace / log 的儲存位置權限  
確認 SDK 與相依函式庫的更新狀況  

5. 常見 NG

實務常見的大致如下。

5.1. 「公司內部工具所以沒問題」

公司內部工具也會普通有壞檔案、誤操作、攜入終端、共享資料夾、舊 DLL、粗略權限設定。 就算沒公開到網際網路,攻擊面也不會消失。

5.2. 「是 HTTPS 所以安全」

HTTPS 重要,但停用憑證驗證意義會相當薄。 另外在更新發佈中不只 HTTPS,也需要 發佈物的真實性確認

5.3. 「加密了所以安全」

解密金鑰的位置、解密權限、使用者邊界、機器邊界沒整理的話,光加密不夠。 特別以 LocalMachine 保護的值當「每使用者的機密」使用,之後會混亂。

5.4. 「日誌多就能調查」

光日誌多,若 token 或個人資訊流出,那本身成為事件。 想要調查性,決定要留什麼不留什麼 比較先。

5.5. 「以管理員跑就解決」

最初輕鬆,但之後在 UAC、發佈、支援、權限邊界、DLL 載入、檔案儲存位置通常會痛苦。 最小權限長期較穩定。

6. 粗略的優先順位

全部一次做太重的話,順序大致如下。

  1. 重新檢視管理員權限 先停止 requireAdministrator 的常用。
  2. 簽章與時間戳 整頓發佈物的可信度。
  3. 機密資訊的撤退 從原始碼、明文設定中移除機密。
  4. HTTPS + 憑證驗證的矯正 從出貨物刪除 => true 系。
  5. 重新檢視 SQL / 檔案 / IPC 輸入 減少字串串接或未驗證輸入。
  6. 固定 DLL 載入 停止只用名稱載入、依賴 PATH。
  7. 日誌的遮罩 讓事故時日誌不成為二次災害。
  8. 相依更新的常態化 做成每次發佈都確認的流程。

這個順序,以「先堵明顯危險的洞」的意義容易推進。

7. 總結

Windows 應用程式開發的安全性,在放入特殊產品或巨大機制之前, 光是整頓 權限、簽章、機密資訊、通訊、輸入、DLL、日誌 的 7 點就會相當改變。

最低限度想掌握的如下。

  • 不要讓整個應用程式以管理員權限執行
  • 對發佈物與更新物簽章、加時間戳
  • 不把機密資訊放在原始碼或明文設定
  • 即使用 HTTPS 也不殺憑證驗證
  • 不信任 SQL、檔案、IPC 等外部輸入
  • 不要讓 DLL 的載入位置曖昧
  • 不要把機密放到日誌
  • 不要放置相依函式庫

安全性的話題廣,但不需要從一開始就全部做。 但 不要把危險的預設行為原樣出貨 的最低限度,值得在相當早的階段就備齊。

8. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽