更新紀錄(2 筆,最後更新 2026年09月04日)
本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。
引用本文(DOI: 10.5281/zenodo.21616275)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
Go Komura(2026)。〈Windows 應用程式開發的最低限度資安檢核表〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616275 https://comcomponent.com/zh-TW/blog/2026/03/14/001-windows-app-security-minimum-checklist/
- DOI(最新版本)
- 10.5281/zenodo.21616275
- DOI(此版本)
- 10.5281/zenodo.22297111
這個檔案的內容與第 4 章的發行前檢核表(8 個分類、32 個項目)相同。差別只在於多了 Status 與 Notes 的填寫欄,以及 Checklist-ja / Checklist-en 兩張工作表同時收錄了日文與英文。邊讀邊確認就看第 4 章,要當成審查紀錄發給別人就用 Excel 版,這樣分工就夠了。
講到 Windows 應用程式的安全性,話題很容易一下子變得很大。 零信任、EDR、SBOM(Software Bill of Materials,軟體物料清單)、憑證維運、漏洞管理。這些都很重要,但在實務上,還有不少更基本、不想漏掉的事情排在它們前面。
尤其是下面這類應用程式,比起「高階防禦」,先把基本面的漏洞堵起來更有效。
- WPF / WinForms / WinUI 的桌面應用程式
- C++ / C# 的 Win32 應用程式
- 設備介接、檔案介接、DB 連線、公司內部散發的工具
- 具備自動更新機制的業務應用程式
- 包含 Windows 服務或輔助 EXE 的組成
在 Windows 應用程式開發中,比起一口氣把所有事情做到完美,先不要留下明顯危險的漏洞比較實際。 以下依照設計、實作、散發、維運的順序,把最低限度不想漏掉的重點整理成方便逐項檢查的形式。
flowchart TB
accTitle: 本文的推進方式
accDescr: 以先堵住基本漏洞而非先做高階防禦為方針,依設計、實作、散發、維運的順序整理最低限度重點的文章流程的圖。
adv1["高階防禦(零信任等)"] -.->|"在這之前"| base1["堵住基本的漏洞"]
base1 --> o1["設計"]
o1 --> o2["實作"]
o2 --> o3["散發"]
o3 --> o4["維運"]
圖 1: 先堵住基本漏洞再談高階防禦,依設計、實作、散發、維運的順序往下看。
1. 先講結論
- 最先不想漏掉的是不要求不必要的系統管理員權限、進行簽署、不以明文持有機密資訊、不停用憑證驗證。
- Windows 應用程式的散發物本身就是攻擊面。連 EXE / DLL / MSI / MSIX / 自動更新模組一起看比較安全。
ServerCertificateValidationCallback => true、明文的連線字串、LoadLibrary("foo.dll")這種隨手的載入方式、以字串串接執行 SQL,都是即使以最低標準來看也想避開的項目。- 如果只有一部分處理需要系統管理員權限,不要讓整個應用程式提升權限,而是只把那一部分切到另一個 EXE 或 service,這樣比較安全。
- 在 Windows 上散發的應用程式,最好以簽署 + 時間戳記為前提來考慮。這不只帶來對使用者的可信度,也讓竄改偵測與維運上的說明都好做。
- 儲存時的機密資訊,依用途分別使用 DPAPI / ProtectedData 或 Credential Locker。至少要脫離以明文放在
appsettings.json的狀態。 - 日誌不是越多越好。token、密碼、連線字串、個人資料、完整的請求內容原樣留著,日誌本身就會變成事故的主角。
最低限度的安全性,重點不在於加上特殊功能,而在於不要留下危險的預設行為與隨手寫成的實作。
flowchart TB
accTitle: 最低限度安全性的思路
accDescr: 最低限度的安全性不是加上特殊功能,而是不留下危險的預設行為與隨手寫成的實作的圖。
add1["加上特殊功能"] -.->|"不是最低限度的重點"| goal1["最低限度的安全性"]
rm1["不留下危險的預設行為與隨手寫成的實作"] -->|"這才是重點"| goal1
圖 2: 最低限度的那條線不是加功能,而是不留下危險的預設行為與隨手寫成的實作。
圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 21 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle
2. 本文的適用範圍與「最低限度」的意思
2.1. 涵蓋的範圍
本文設想的是這類 Windows 應用程式。
- WPF / WinForms / WinUI 的桌面應用程式
- C++ / C# 的 Win32 應用程式
- 公司內部散發的工具、設備介接工具、監控工具
- 包含輔助 EXE、Windows 服務、更新程式的組成
- 以 EXE / MSI / MSIX 散發的業務用軟體
這裡說的「最低限度」,不是通得過稽核的最終形態,而是少了它就會正常出事的項目。
flowchart TB
accTitle: 本文中「最低限度」的意思
accDescr: 本文的最低限度不是指通得過稽核的最終形態,而是指少了就會正常出事的項目的圖。
au1["通得過稽核的最終形態"] -.->|"這裡不是指這個"| mn1["本文的「最低限度」"]
ac1["少了就會正常出事的項目"] -->|"指的是這個"| mn1
圖 3: 「最低限度」指的不是應付稽核的最終形態,而是少了就會正常出事的那些項目。
程式碼範例的前提也先講清楚。C# 的例子以 .NET 8 以後為準,C++ 的例子以 Win32 API 為準。在 .NET Framework 4.8 上想法一樣,但有些地方建議的寫法已經改了。代表性的例子是 3.6 的 ServicePointManager 一帶,新程式碼的前提已經改成使用 IHttpClientFactory 與 HttpClient。重讀舊世代的程式碼時,只有這一點要特別注意。
2.2. 不涵蓋的範圍
另一方面,也有一些東西不放在本文的中心。
- 企業整體的零信任設計
- EDR / SIEM / DLP / MDM 的整體維運
- 核心驅動程式的詳細強化
- 從零開始做密碼學設計本身
- 進階的威脅分析與數位鑑識流程
也就是說,本文處理的不是「組織層級的龐大資安措施」,而是 Windows 應用程式開發者在發行前靠自己就不該漏掉的基本線。
flowchart TB
accTitle: 涵蓋範圍與不涵蓋範圍的界線
accDescr: 把組織層級的龐大資安措施排除在外,以 Windows 應用程式開發者在發行前靠自己就不該漏掉的基本線為對象的範圍界線的圖。
org1["組織層級的龐大措施"] -.->|"本文不處理"| sc1["本文的範圍"]
dev1["開發者靠自己就不該漏掉的基本線"] -->|"處理這個"| sc1
圖 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") 交給目前的工作目錄決定 |
| 日誌 | 遮罩 token、密碼、PII,並區分給使用者看的錯誤訊息 | 直接顯示或儲存例外的詳細內容與連線字串 |
| 相依性 | 持續更新 SDK、NuGet、VC++ 執行階段與 OSS 相依項目 | 固定好幾年不動,也不追漏洞資訊 |
3.2. 權限以 asInvoker 為基本
Windows 應用程式最先要重新檢視的就是這裡。 把整個應用程式用系統管理員權限執行,bug、DLL 被掉包、設定檔誤讀、外部輸入處理不周,全都會直接以強權限執行。
flowchart TB
accTitle: 整個應用程式都用系統管理員權限執行的危險
accDescr: 把整個應用程式用系統管理員權限執行時,bug、DLL 掉包、設定檔誤讀、外部輸入處理不周都會直接以強權限執行的圖。
all1["整個應用程式用系統管理員權限執行"] --> bug1["bug"]
all1 --> swp1["DLL 掉包"]
all1 --> inp1["設定誤讀、輸入不周"]
bug1 --> pw1["直接以強權限執行"]
swp1 --> pw1
inp1 --> pw1
圖 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>
「用系統管理員權限跑比較輕鬆」這件事,之後大多會反過來咬你一口。 以最小權限執行,再只把真正必要的操作切出來,災情範圍會小很多。
flowchart TB
accTitle: 只把需要提升權限的處理分離出去的架構
accDescr: 一般的 UI 應用程式以 asInvoker 執行,只把需要系統管理員權限的處理收攏到另一個 EXE 或 service 的 broker,並且只在必要的瞬間提升權限的架構的圖。
ui1["UI 應用程式(asInvoker)"] -->|"只在必要的瞬間委託"| br1["broker(另一個 EXE / service)"]
br1 --> el1["只執行需要提升權限的處理"]
ui1 -.-> vd1["傳給 broker 的輸入也要驗證"]
圖 6: 平常以 asInvoker 執行,只把需要提升權限的處理收攏到 broker,災情範圍就會縮小。
3.3. 對二進位檔與安裝程式簽署
在 Windows 上,散發物的可信度很關鍵。 使用者碰到的不是原始碼,而是 EXE、DLL、MSI、MSIX、更新程式。這裡如果沒有簽署,維運上的說明、竄改偵測、散發時的安心感都會變弱。
flowchart TB
accTitle: 使用者碰到的是散發物
accDescr: 使用者碰到的不是原始碼而是 EXE、DLL、安裝程式與更新程式,這些沒有簽署時竄改偵測與維運上的說明都會變弱的圖。
us1["使用者會碰到的東西"] --> bin1["EXE / DLL"]
us1 --> pkg1["MSI / MSIX"]
us1 --> upd1["更新程式"]
bin1 --> ns1["未簽署則竄改偵測與說明都很弱"]
pkg1 --> ns1
upd1 --> ns1
圖 7: 成為攻擊面的是散發物本身,維持未簽署就會失去可信度的依據。
至少想先看的是這幾點。
- 對 EXE / DLL / MSI / MSIX 簽署
- 不只是安裝程式,更新時使用的輔助執行檔也要簽署
- 加上時間戳記
- 把憑證的有效期限與更新流程納入 release 流程
尤其是沒有加時間戳記的簽章,在憑證過期之後做驗證時特別容易出問題。 不要停在「簽了就結束」,把簽署 + 時間戳記都納入 release 流程會比較穩定。
flowchart TB
accTitle: 簽署與時間戳記
accDescr: 沒有加時間戳記的簽章在憑證過期後驗證時容易出問題,把簽署與時間戳記都納入 release 流程會比較穩定的圖。
sg2["只有簽署"] --> tr1["憑證過期後驗證容易出問題"]
ts1["簽署 + 時間戳記"] --> st2["過期後驗證不容易出問題"]
ts1 -.-> fl1["先納入 release 流程"]
圖 8: 不要只做到簽署,連時間戳記一起納入 release 流程。
如果要用 MSIX,套件簽署是前提。 即使是 MSI / EXE 散發,至少也該把安裝程式本體與主要的執行檔簽署起來。
3.4. 固定更新路徑,加入竄改偵測
現在的 Windows 應用程式,更新路徑被使用的時間比初次安裝長得多。 這裡做得隨便,就算本體做得再仔細,更新程式也會變成最弱的一環。
flowchart TB
accTitle: 更新路徑被使用的時間最長
accDescr: 更新路徑被使用的時間比初次安裝長,因此更新這一帶做得隨便時更新程式會成為整個應用程式最弱一環的圖。
ins1["初次安裝"] -.->|"只用一次"| ap1["應用程式的生命週期"]
up2["更新路徑"] -->|"會長期被持續使用"| ap1
up2 --> wk2["做得隨便就成為最弱的一環"]
圖 9: 更新路徑比初次安裝用得更久,做得隨便就會變成最弱的一環。
更新這一塊最低限度要先想清楚的是這 5 點。
- 更新檔的取得以 HTTPS 為前提
- 驗證下載回來的更新物的簽章或雜湊
- 不要讓更新來源 URL 能透過程式碼或設定被無限制地換掉
- 更新模組本身也要簽署
- 決定好 rollback 以及失敗時的復原流程
如果能採用 MSIX + App Installer,更新的機制就比較容易靠向 OS 那一側。 反過來說,如果自己做更新程式,就必須同時確認通訊的安全性與散發物的真正性。HTTPS 只守得住「通訊路徑」,並不保證「這個檔案真的是自己發行的東西」。
flowchart TB
accTitle: 更新時要確認的兩件事
accDescr: 自製更新程式時必須同時確認 HTTPS 帶來的通訊安全性,以及下載回來的更新物以簽章或雜湊確認的真正性的圖。
dl1["以 HTTPS 取得更新檔"] --> vf1["驗證簽章或雜湊"]
vf1 --> ap2["只套用通過驗證的檔案"]
dl1 -.-> lim1["HTTPS 守的只有通訊路徑"]
vf1 -.-> own1["確認是不是自己發行的東西"]
圖 10: 就算用 HTTPS 取得,真正性仍是另一回事,要通過簽章或雜湊的驗證才套用。
3.5. 不要把機密資訊放在原始碼或明文設定裡
這裡在實務上真的很容易出事。 常常會以「反正是公司內部工具」「反正只是發個 exe 出去」為由,把連線字串、API 金鑰、共用資料夾的認證資訊、固定 token 放進原始碼或設定檔。
至少這幾種放法要避開。
- 直接寫死在原始碼裡的 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 則是同一台電腦上的任何人都能解密。如果要以服務的身分用另一個帳戶執行,或維運上會切換使用者,這裡不先決定好,之後就會卡在「讀不到」或「不該讀到的人讀得到」其中一邊。
flowchart TB
accTitle: DPAPI 中誰能解密
accDescr: DPAPI 的範圍設成 CurrentUser 時只有儲存的使用者能解密,設成 LocalMachine 時同一台電腦上的任何人都能解密,因此必須在設計時先決定誰能解密的圖。
dc1["在設計時決定誰能解密"] -->|"CurrentUser"| cu1["只有儲存的使用者能解密"]
dc1 -->|"LocalMachine"| lm1["同一台電腦上的任何人都能解密"]
dc1 -.-> lt1["不先決定,之後就會卡住"]
圖 11: DPAPI 會因為範圍的選法而改變可解密的對象,所以要先決定「誰能解密」。
另外,DPAPI 的金鑰放在使用者設定檔裡,因此文件上明確寫著,在設定檔尚未載入的狀態(例如 impersonation 期間)解密會失敗。如果打算從服務使用,這一點也要先確認。
flowchart TB
accTitle: DPAPI 的金鑰與使用者設定檔
accDescr: DPAPI 的金鑰放在使用者設定檔裡,因此在 impersonation 期間等設定檔尚未載入的狀態下解密會失敗的圖。
ky1["DPAPI 的金鑰"] --> pf1["位於使用者設定檔中"]
pf1 -->|"設定檔未載入(例如 impersonation 期間)"| fe1["解密失敗"]
fe1 -.-> sv2["從服務使用時需要確認"]
圖 12: DPAPI 的金鑰放在使用者設定檔裡,設定檔沒有載入的狀態下就無法解密。
如果是 SQL Server 連線,在地端環境有時可以把 Windows 驗證當成第一選擇。
若真的必須把認證資訊寫進連線字串,至少要維持 Persist Security Info=False,而且不要就這樣一直放在明文設定檔裡。
3.6. 通訊以 HTTPS 為前提,不要把憑證驗證關掉
原本只打算在開發期間用的後門,就這樣留到正式環境。 通訊相關的事故,大多是這個模式。
特別容易留在出貨成品裡的,是這類程式碼或設定。
ServicePointManager.ServerCertificateValidationCallback += ... => trueHttpClientHandler.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。也就是說,某處一行的 => true,有可能連 HttpClient 的通訊也一起放行。
flowchart TB
accTitle: 舊的回呼影響現在通訊的路徑
accDescr: ServicePointManager 的 ServerCertificateValidationCallback 在 .NET 9 以後會對應到 SocketsHttpHandler 的驗證回呼,因此某處一行的回傳 true 有可能連 HttpClient 的通訊也一起放行的路徑的圖。
old1["ServicePointManager 的驗證回呼"] -->|".NET 9 以後會對應過去"| new1["SocketsHttpHandler 側的驗證"]
new1 --> ef1["對 HttpClient 的通訊也有作用"]
ef1 -.-> rk2["一行的回傳 true 就可能掏空整個通訊"]
圖 13: 放在舊 API 上的驗證跳過,會透過對應關係讓現在 HttpClient 的通訊也一起放行。
要例外放寬驗證時,不要用會影響整個處理程序的全域設定,而是做成只封閉在那個處理常式(handler)內的形式。
// 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;
}
// 就算是釘選的那一張,過期與失效也不承認。
// 一旦承認,就會變成「因為金鑰外洩而撤銷的憑證」還能繼續使用的路徑
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,那麼即使那張憑證已經過期,或是金鑰外洩之後已經撤銷,也會一直通過。釘選的意思是「只信任這一張」,不是「只要是這一張,出什麼事都信」。上面的程式碼允許的只有 UntrustedRoot 與 PartialChain(=公司內部 CA 沒有裝進這台電腦),NotTimeValid(過期)與 Revoked(已失效)則會直接拒絕。
要查失效狀態就需要 CheckCertificateRevocationList = true(預設是 false,不會確認失效)。反過來說,如果公司內部 CA 既沒有公開 CRL 也沒有 OCSP,就會因為 RevocationStatusUnknown 而被擋下來。那才是正確的行為。如果無法準備失效的確認手段,就用縮短憑證的有效期間,或事先做好可以重新發送釘選值的路徑,其中一種來補上。「無法撤銷卻釘選了長期憑證」是最危險的狀態。
flowchart TB
accTitle: 釘選時的判定流程
accDescr: 沒有錯誤就放行,鏈結錯誤以外一律拒絕,比對主機名稱與指紋,即使鏈結狀態也會拒絕過期與失效的例外驗證判定流程的圖。
e0["確認 errors"] -->|"None"| pass1["放行"]
e0 -->|"鏈結錯誤以外"| rj1["拒絕"]
e0 -->|"只有鏈結錯誤"| hchk["比對主機名稱與指紋"]
hchk -->|"不一致"| rj1
hchk -->|"一致"| cchk["確認鏈結狀態"]
cchk -->|"過期、失效等"| rj1
cchk -->|"只有未導入公司內部 CA 的狀態"| pass1
圖 14: 釘選之後也不當成沒看到的判定流程,過期與失效都會直接被拒絕。
3.7. 把外部輸入全部當成「不可信任的輸入」
Windows 應用程式不是 Web 應用程式,輸入 validation 很容易變鬆。 但實際上,外部輸入的入口比想像中多。
- 檔案路徑
- CSV / Excel / JSON / XML
- 命令列引數
- named pipe / socket / COM / RPC / gRPC
- 傳給 DB 的字串
- 登錄檔的值
- 剪貼簿
- URL / deep link
- 從外部設備或 SDK 回傳的資料
其中最不想漏掉的是下面 3 點。
- SQL 一定要參數化 不要用字串串接組 SQL。
- 檔案路徑先正規化再使用 不要把使用者指定的路徑直接拿去刪除、覆寫、解壓縮。
- 讀取外部檔案時要加上大小上限與格式檢查 「打得開」不等於「安全」。
以 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,都會正常地跑進來。
flowchart TB
accTitle: 公司內部工具一樣會收到壞掉的輸入
accDescr: 公司內部工具一樣會正常收到壞掉的 CSV、非預期的檔名、舊的 DB 資料、手動輸入錯誤與半成品 JSON,因此外部輸入全部都當成不可信任的輸入處理的圖。
csv1["壞掉的 CSV"] --> in2["應用程式的輸入"]
fn1["非預期的檔名"] --> in2
hm1["手動輸入錯誤、舊資料"] --> in2
in2 --> tr2["全部當成不可信任的輸入來驗證"]
圖 15: 公司內部工具一樣會收到壞掉的輸入,所以要在每個入口驗證過再使用。
3.8. 不要讓 DLL 的載入來源含糊不清
這是很有 Windows 味道的陷阱。
像 LoadLibrary("foo.dll") 這樣只用名稱載入 DLL,會因為搜尋順序而抓到非預期位置的 DLL。
該做的事很明確。
- 可以的話就指定 DLL 的絕對路徑
- 在很早的階段就設定
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - 用
AddDllDirectory明確加入搜尋對象 - 避免把
SearchPath的結果直接丟給LoadLibrary的設計 - 不要完全依賴 safe DLL search mode
舉例來說,如果是原生程式碼,在處理程序初始化的很早階段放入下面這一行是很值得採用的做法。
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
然後只把必要的額外目錄用 AddDllDirectory 註冊進來。
這裡因為「平常都會動」所以很容易被放著不管,但只要散發目的地的工作目錄變了,或其他產品的 DLL 進了 PATH,就會安靜地壞掉。 不只是安全性,作為故障預防也相當有用。
flowchart TB
accTitle: 固定 DLL 的載入來源
accDescr: 只用名稱載入 DLL 會因為搜尋順序而抓到非預期位置的 DLL,因此在很早的階段設定 SetDefaultDllDirectories、用 AddDllDirectory 明確指定搜尋對象、可以的話指定絕對路徑的對策的圖。
nm1["只用名稱載入"] --> pick2["因搜尋順序抓到非預期的 DLL"]
fix1["及早設定 SetDefaultDllDirectories"] --> add2["以 AddDllDirectory 明確加入"]
add2 --> abs1["可以的話指定絕對路徑"]
abs1 --> safe1["載入來源不再含糊"]
圖 16: 停止交給名稱決定的載入,明確指定搜尋對象,把載入來源固定下來。
3.9. 不要在日誌與例外裡輸出機密
為了排查故障而增加日誌很重要。 但日誌也很容易變成機密的墳場。
日誌這一塊最低限度想重新檢視的有以下幾點。
- 不要把密碼、Bearer token、API 金鑰寫進日誌
- 不要把連線字串整串輸出
- 個人資料與業務資料的內容要遮罩
- 例外的詳細內容,要區分給使用者看的畫面與內部日誌
- 不要在正式環境啟用 debug 用的 PII 日誌
- 重新檢視 dump 與 trace 的儲存位置權限
最近的 .NET 也比較容易以 redaction 為前提來整理。 至少「什麼都轉成字串然後直接 log」這種做法要停掉。
舉幾個常見的失敗。
- 把 HTTP request / response body 整個存下來
- 驗證失敗時輸出 token 或整個標頭
- 把例外訊息原樣顯示在 MessageBox
- 把機密日誌全部打包進維護用 ZIP
錯誤顯示可以像下面這樣區分。
- 給使用者:「連線到伺服器失敗。請檢查網路設定與 URL。」
- 內部日誌:失敗的目標主機、TLS 錯誤種類、關聯 ID、stack trace、重試次數
光是做這個分離,資訊外洩與可追查性之間的平衡就會好很多。
flowchart TB
accTitle: 錯誤顯示的分流
accDescr: 發生錯誤時對使用者只顯示簡潔的說明,內部日誌則留下失敗的目標主機、錯誤種類、關聯 ID 與 stack trace 等調查資訊的分流的圖。
er1["發生錯誤"] --> usr1["給使用者:只有簡潔的說明"]
er1 --> lg2["內部日誌:主機、種類、關聯 ID 等"]
lg2 -.-> bl1["兼顧防止外洩與可追查性"]
圖 17: 光是把給使用者的顯示和內部日誌分開,外洩與可追查性的平衡就會變好。
3.10. 不要放著相依函式庫與開發工具不管
最後這一項不起眼,但效果很大。 就算應用程式本體做得很仔細,只要還載著舊的執行階段或有已知漏洞的相依函式庫,根基就會被抽掉。
要看的項目本身不多。
- 把 .NET SDK / runtime 保持在支援期限內的版本
- 定期確認 NuGet / OSS 相依項目的更新
- 用 C++ 的話要管理可轉散發套件與外部 DLL 的版本
- 把漏洞資訊的確認放進發行前檢查
- 準備 smoke test,避免相依更新之後壞掉
這裡最危險的就是「之後再一起做」。 放個半年、一年,更新的差異就會變得太大,資安因應本身就變成一件大工程。
flowchart TB
accTitle: 放著相依更新不管的下場
accDescr: 把相依性的更新放著半年到一年不管,更新的差異就會變得太大,資安因應本身變成一件大工程的流程的圖。
pt1["之後再一起做"] --> ac2["放置半年~1 年"]
ac2 --> df1["更新的差異變得太大"]
df1 --> hw1["因應本身變成大工程"]
rg1["定期確認"] -.->|"可以避開這個"| hw1
圖 18: 相依項目的更新越是放著不管,差異就越膨脹,連因應本身都變成一件大工程。
3.11. 各項目要怎麼確認
檢核表要和確認方法成套,才真正發揮作用。針對 3.2 到 3.10 的項目,這裡列出發行前實際可以執行的做法。
| 想確認的事 | 確認方法 |
|---|---|
| 有沒有要求提升權限 | 在應用程式的資訊清單裡查看 requestedExecutionLevel 的值。有原始碼就看 app.manifest,只有散發物就用 Sysinternals 的 Sigcheck 或資源編輯器確認 |
| 簽章與時間戳記 | 在 PowerShell 執行 Get-AuthenticodeSignature .\app.exe,查看 Status 是不是 Valid、有沒有 TimeStamperCertificate。隨附的 DLL 與 updater 也要一個一個看 |
| 有沒有把憑證驗證關掉 | 用 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 件」都記下來,下一次發行就只要看差異。
flowchart TB
accTitle: 把確認留成紀錄的效果
accDescr: 發行前的確認即使結果是 0 件也留成紀錄,下一次發行就只要看差異的流程的圖。
chk2["發行前執行確認"] --> rec1["連搜尋過了、0 件都記下來"]
rec1 --> nx1["下一次發行只要看差異"]
圖 19: 連「確認過」這個事實都記下來,下一次開始就只要確認差異。
4. 發行前檢核表
做成可以直接當作審查或出貨判定範本的形式。 為了方便用表格確認,把發行前最低限度想看的項目依分類排出來。
4.1. 權限、執行方式
| 檢核項目 | 確認 | 備註 |
|---|---|---|
一般啟動時以 asInvoker 執行 |
□ | |
| 需要系統管理員權限的處理已分離到另一個 EXE / service 等 | □ | |
| 使用 service 時,沒有給超出必要的強力執行帳戶 | □ | |
已區分 %ProgramFiles% 底下與使用者資料底下的職責 |
□ |
4.2. 散發、簽署
| 檢核項目 | 確認 | 備註 |
|---|---|---|
| 已對 EXE / DLL / MSI / MSIX / updater 簽署 | □ | |
| 簽章有加上時間戳記 | □ | |
| 憑證的有效期限與更新流程已納入 release 流程 | □ | |
| 已決定散發物的雜湊確認或竄改偵測方式 | □ |
4.3. 更新
| 檢核項目 | 確認 | 備註 |
|---|---|---|
| 更新的取得以 HTTPS 進行 | □ | |
| 下載之後會驗證簽章或雜湊 | □ | |
| 設計上不容易任意替換更新來源 URL | □ | |
| 有更新失敗時的 rollback 或重試方針 | □ |
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 載入、檔案儲存位置上大概都會很難受。 長期來看,最小權限比較穩定。
flowchart TB
accTitle: 「用系統管理員權限跑就解決了」的下場
accDescr: 常態使用系統管理員權限一開始雖然輕鬆,之後會在 UAC、散發、支援、權限邊界、DLL 載入與檔案儲存位置上變得難受,長期來看最小權限比較穩定的圖。
ez1["用系統管理員權限跑就解決了"] --> ez2["一開始很輕鬆"]
ez2 --> pain1["在 UAC、散發、支援上變得難受"]
ez2 --> pain2["在權限邊界、儲存位置上變得難受"]
lp1["以最小權限執行"] -->|"長期來看穩定"| ok2["可以避開這些難受"]
圖 20: 常態使用系統管理員權限只有一開始輕鬆,長期來看最小權限比較穩定。
6. 大致的優先順序
如果要一口氣全部做完太重,優先順序大致是這樣。
排序的基準,是出事時災情的大小與修正成本的低廉兩者相乘。第 3 章是照「權限 → 散發 → 實作 → 維運」這個設計流程排的,這裡則是「從危險的開始」,所以和章節順序不一致。下面附上對應的小節。
- 重新檢視系統管理員權限(3.2)
先停止常態使用
requireAdministrator。受害範圍會差一個等級,但作為設計變更,多半改動不大。 - 簽署與時間戳記(3.3) 把散發物的可信度整理好。只要納入流程就好,事後才加就得重新散發。
- 把機密資訊移出去(3.5) 把祕密從原始碼與明文設定裡拿掉。外洩時的損害很大,而且洩漏之後救不回來。
- HTTPS + 修正憑證驗證(3.6)
把
=> true這一類從出貨成品裡刪掉。多半刪掉就修好了,放著不管的話整條通訊都不可信。 - 重新檢視 SQL / 檔案 / IPC 的輸入(3.7) 減少字串串接與未驗證的輸入。件數多,但可以一處一處改。
- 固定 DLL 的載入來源(3.8) 停止只用名稱載入、交給 PATH 決定。這是修改啟動處理的一部分,也能預防故障。
- 日誌的遮罩(3.9) 讓事故發生時的日誌不要變成後續衍生的災情。輸出的地方多,所以會花時間。
- 把相依更新變成常態(3.10) 做成每次發行都確認的流程。不是做一次就結束,把它變成機制才算完成。
更新路徑(3.4)沒有排進這個順序,是因為也有不具自動更新的應用程式。如果有,就請以和第 2 項簽署相同的優先度來看。更新模組比本體弱,卻站在可以改寫本體的位置上。
flowchart TB
accTitle: 優先順序的排法
accDescr: 以出事時災情的大小與修正成本的低廉相乘,從危險的開始排,具備自動更新的應用程式則把更新路徑視為與簽署同等優先度的思路的圖。
dmg1["災情的大小"] --> mul1["以相乘決定順序"]
cst1["修正成本的低廉"] --> mul1
mul1 --> ord1["從危險的開始逐一堵住"]
ord1 -.-> upn1["有自動更新的話,更新路徑與簽署同等優先"]
圖 21: 優先順序由災情大小與修正成本相乘決定,從危險的漏洞開始堵。
照這個順序走,比較容易以「先堵住明顯危險的漏洞」的方式往前推進。
7. 總結
Windows 應用程式開發的安全性,在導入特別的產品或龐大的機制之前, 光是把權限、簽署、機密資訊、通訊、輸入、DLL、日誌這 7 點整理好,就會有相當大的改變。
把最低限度的那條線各用一句話說完,會是這樣。
- 不要讓整個應用程式以系統管理員權限執行
- 對散發物與更新物簽署,並加上時間戳記
- 不要把機密資訊放在原始碼或明文設定裡
- 就算用了 HTTPS,也不要把憑證驗證關掉
- 不要信任 SQL、檔案、IPC 等外部輸入
- 不要讓 DLL 的載入來源含糊不清
- 不要在日誌裡輸出機密
- 不要放著相依函式庫不管
資安的話題很廣,但不需要一開始就全部做完。 只是不要把危險的預設行為就這樣出貨這個最低限度,值得在相當早的階段就先備齊。
8. 參考資料
- Administrator Broker Model - Win32 apps
- How User Account Control works
- Authenticode Digital Signatures
- Time Stamping Authenticode Signatures
- Sign a Windows app package
- Credential Locker for Windows apps
- CryptProtectData function (dpapi.h)
- CA5359: Do not disable certificate validation
- CA5399: Enable HttpClient certificate revocation list check
- Configuring parameters - ADO.NET Provider for SQL Server
- Connection String Syntax - ADO.NET
- Dynamic-Link Library Security - Win32 apps
- SetDefaultDllDirectories function (libloaderapi.h)
- Data redaction in .NET
- ProtectedData Class - 說明只支援 Windows,以及設定檔未載入時的注意事項。
- ServicePointManager.ServerCertificateValidationCallback - .NET 9 以後會對應到 SocketsHttpHandler 的設定。
- dotnet list package 命令 - 可以用
--vulnerable確認已知漏洞。 - Get-AuthenticodeSignature
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
非預期例外發生時該結束還是繼續執行的判斷表
發生非預期的例外時,應該讓應用程式結束還是繼續執行?本文從狀態毀損、外部副作用、執行緒與原生邊界的角度梳理判斷方式。
在 Windows 應用程式中只分離「需要系統管理員權限的處理」的具體寫法
在 Windows 應用程式中讓 UI 維持 asInvoker,只把需要系統管理員權限的處理分離到 helper EXE。本文把這套設計連同 UAC、runas、具名管道、輸入驗證一起具體梳理清楚。
Windows 應用程式的機密資訊保存 - 用 DPAPI 避免明文設定
為了不讓 Windows 應用程式把連線資訊或 API 權杖以明文存進設定檔,本文梳理 DPAPI / ProtectedData 的思路、CurrentUser 與 LocalMachine 的差異,以及實作時的注意事項。
具名管道實務 ── 從設計到安全,看懂 Windows 行程間通訊的標準做法
以實務角度說明 Windows 行程間通訊的標準做法——具名管道。依據一手資料整理位元組模式與訊息模式的取捨、同時接受多個用戶端的伺服器結構、ACL 與模擬的安全設計,以及 .NET 的具名管道串流。
ADR(Architecture Decision Record)入門 ── 在小規模開發中留下「為何採用此設計」的最小做法
程式碼不會說明「為什麼這樣做」。本文說明如何用ADR(Architecture Decision Record)以「1個決定=1個檔案」的Markdown留下設計判斷的理由,並附上範本、該寫/不該寫的判斷表與實例。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
這個主題涵蓋權限設計、散發方式、更新方式一直到日誌設計,屬於重新檢視整個 Windows 應用程式的討論,和 Windows 應用程式開發相當契合。
技術諮詢 & 設計審查
如果想從既有應用程式的資安重新檢視、權限邊界的梳理,或更新程式方針的重新設計開始著手,可以整理成技術諮詢與設計審查來進行。
常見問題
整理諮詢這個主題時常見的問題。
- Windows 應用程式的安全性最先該看的是什麼?
- 不要求不必要的系統管理員權限、進行程式碼簽署、不以明文持有機密資訊、不停用憑證驗證,這 4 點。最低限度的安全性,重點不在於加上特殊功能,而在於不留下危險的預設行為與隨手寫成的實作。常態跳過憑證驗證、明文的連線字串、交給目前的工作目錄決定的 DLL 載入、以字串串接執行 SQL,都是即使以最低標準來看也想避開的項目。
- 不能讓整個應用程式以系統管理員權限執行嗎?
- 應該避免。把整個應用程式用系統管理員權限執行,bug、DLL 被掉包、設定檔誤讀、外部輸入處理不周,都會直接以強權限執行。一般的 UI 應用程式以 asInvoker 為基本,只把需要系統管理員權限的處理分離到另一個處理程序或 service,並且只在必要的瞬間提升權限,這才是基本方針。
- API 金鑰與連線字串應該儲存在哪裡?
- 首先要脫離以明文放在原始碼或 appsettings.json 等設定檔的狀態。儲存時的機密資訊,依用途分別使用 DPAPI / ProtectedData 或 Credential Locker。另外,把 token、密碼、連線字串、個人資料原樣留在日誌裡,日誌本身就會變成事故的主角,因此需要遮罩。
- 公司內部散發的應用程式也需要程式碼簽署嗎?
- 最好當成前提。在 Windows 應用程式上,散發物本身(EXE / DLL / MSI / MSIX / 自動更新模組)就是攻擊面。有程式碼簽署加時間戳記,就能得到竄改偵測、對使用者的可信度,以及維運上容易說明這三件事。更新機制也應該固定更新來源,做成能以 HTTPS 與簽章確認偵測竄改的形式。