💡 完整的工作範例可在 GitHub 上取得:
skip-external-resources-when-signing-dotnet
介紹
Word 文件可能包含一張不在檔案內的圖片。文件只保存了地址,任何開啟它的程式都會去抓取該地址。在桌面上這是一項功能——當來源更新時圖片會同步更新。但在接受上傳的伺服器上,這意味著傳送檔案的人決定了你的基礎設施會請求哪些 URL。
安全的文件載入是 GroupDocs.Signature 在 .NET 中的行為,它會拒絕發出這些請求。從 26.9 版開始,LoadOptions.SkipExternalResources 預設為 true。本文比較了三種載入模式對同一文件的影響,說明如何只允許單一主機而不允許全部,並闡述為何對不受信任的檔案簽名根本不需要任何網路存取。
為何這比聽起來更重要
此攻擊有名稱——伺服器端請求偽造(SSRF)——且有三種具體形態。
- 內部地址在互聯網上無法存取,但你的伺服器可以存取,於是精心製作的文件可以讓你的服務去抓取
http://169.254.169.254/或本機的管理端點,根據你對結果的處理方式,可能會洩漏資訊。 - 文件中的 UNC 路徑會促使 Windows 主機向外部驗證,將憑證交給攻擊者控制的伺服器。
- 指向永遠不回應的主機的連結會使載入執行緒卡住直至逾時,這是一種廉價的耗盡工作池的方式。
我原本以為這只是理論上的顧慮,直到看到測試文件透過一個根本不該發出外部請求的服務拉取圖片。這一切都不需要文件庫本身的漏洞。跟隨連結是格式的要求;問題在於你的伺服器是否應該配合。
方法 1 ─ 新的預設
完全不使用 LoadOptions:
using var signature = new Signature(sourcePath);
return SavePagePreview(signature, previewPath);
不會抓取任何資源。預覽會在連結圖片的位置顯示空白佔位,且產生的 PNG 檔案會比原本小。這個大小差異是最直接的證明,表示沒有任何請求離開機器。
哪些特性算作外部資源?連結圖片(而非內嵌的)、INCLUDEPICTURE 欄位、簡報與試算表中的連結圖片,以及 SVG 參考的圖片與樣式表。內嵌內容不受影響——它已經在檔案內。
方法 2 ─ 白名單單一地址
許多文件會連結到合法的來源:公司 CDN、內部圖片伺服器、模板庫。只允許該地址,其他全部拒絕:
var loadOptions = new LoadOptions
{
WhitelistedResources = new List<string> { trustedAddress }
};
using var signature = new Signature(sourcePath, loadOptions);
匹配規則值得注意。它是對資源地址執行不分大小寫的子字串測試,這意味著短片段很危險:github 會同樣匹配 github.attacker.example/payload.png。請使用協議、主機與路徑──範例白名單為 raw.githubusercontent.com/groupdocs-signature/。
方法 3 ─ 允許全部
26.9 之前的行為仍可使用:
var loadOptions = new LoadOptions { SkipExternalResources = false };
對於你自己的應用程式產生的文件而言是合理的。需要提醒的一個陷阱是:已棄用的 LoadExternalResources 屬性極性相反,因此 SkipExternalResources = false 等同於 LoadExternalResources = true。若直接把舊屬性的值複製過來,會在不發出任何錯誤的情況下顛倒你的安全姿態。
三種模式比較:何時使用哪一種
| 模式 | 最適用情境 | 主要優勢 | 限制 |
|---|---|---|---|
| 預設(跳過) | 使用者上傳、電子郵件、合作夥伴檔案 | 不會產生任何外部請求 | 連結圖片會顯示為佔位 |
| 白名單 | 連結到你擁有的主機的文件 | 讓合法連結仍能正常工作 | 子字串匹配需要足夠長且具體的片段 |
| 允許全部 | 你的系統自行產生的檔案 | 預覽與原始完全相同 | 重新開啟了預設移除的 SSRF 風險 |
簽名時需要這些資源嗎?
不需要,這也是實際的好處。使用預設載入設定套用 QR Code 簽名時,整個載入、簽名或儲存過程都不會請求外部資源:
var options = new QrCodeSignOptions("Approved by GroupDocs.Signature")
{
EncodeType = QrCodeTypes.QR,
Left = 400,
Top = 50,
Width = 120,
Height = 120
};
SignResult result = signature.Sign(outputPath, options);
簽名後的輸出仍保留原有連結,因此之後使用者開啟文件時,仍會在自己的機器上解析圖片。跳過是伺服器端的政策,而非對文件的編輯──這正是它在代他人處理文件時安全的原因。
升級時會有哪些變化
對大多數服務而言,表面上看不出任何變化,這點值得直白說明,因為安全預設若在所有地方改變行為,升級審查時很難通過。唯一例外是:先前會顯示連結圖片的預覽或縮圖,現在會顯示佔位;這正是預設發揮作用的結果,若該主機屬於你,解決方式是加入白名單;若文件來自外部,則接受佔位即可。
最直接的檢查方式就是範例所示:在三種模式下渲染同一文件,比較輸出檔案大小。若預設與白名單預覽的大小相同,表示兩者皆未抓取任何資源──通常代表該主機對該機器不可達,而非白名單失效,範例會印出相應提示。
預覽輔助程式——因為不那麼明顯
上述三種模式中有兩種會呼叫一個小輔助程式,值得展示,因為 PreviewOptions 本身不接受路徑參數:
var previewOptions = new PreviewOptions(
pageData => File.Create(previewPath),
(pageData, pageStream) => pageStream.Dispose())
{
PreviewFormat = PreviewOptions.PreviewFormats.PNG
};
signature.GeneratePreview(previewOptions);
它接受兩個串流工廠──一個用於為每頁建立串流,另一個用於釋放串流。範例文件只有一頁,所以只會寫入一個檔案;若輸入為多頁,請在檔名中加入頁碼,否則每頁都會覆寫前一頁。
最佳實踐
- 對任何非自行產生的檔案視為不受信任,包括來自安全姿態良好的合作夥伴的文件。
- 讓白名單片段足夠長以免產生歧義,且在 CDN 變更時重新檢視。
- 絕不要把
SkipExternalResources設為從LoadExternalResources複製過來的值。 - 以輸出檔案大小驗證,而非僅依賴設定;看起來正確的設定與實際未發出請求是兩回事。
SVG 的處理
特別說明,因為 SVG 同時是常見的上傳格式與常見的 SSRF 向量。SVG 可以透過 URL 參考圖片與樣式表,這些參考同樣屬於外部資源──預設會被跳過、可加入白名單、亦可恢復。接受 SVG 頭像或標誌並在伺服器端渲染的服務正是此變更要保護的典型情境。
如果你的管線接受使用者上傳的 SVG,預設設定就是你想要的;白名單則用於你的模板需要從你自行管理的主機取得共享樣式表的情況。
結論
預設已改為需要明確決策才能執行風險行為,而安全的行為則不需要任何額外設定。對不受信任的輸入保留預設,對自有主機使用窄範圍白名單,並記住簽名本身從未需要網路。將範例對你的任意文件執行一次,只需一分鐘,即可透過三個檔案大小告訴你服務到底抓取了什麼。