從安全角度理解 Pillow 的 open()verify()load()

圖像上傳不只是「接收圖檔」,而是將外部輸入送入解碼器(解析器)進行處理。 因此 Pillow 的三個方法,重點不在功能說明,而在於何時呼叫(即何時開啟攻擊面)。


open() 不是「把像素拉起來」的函式

Image.open()延遲執行。它只會開啟檔案並辨識格式,像素資料可能尚未讀取,且檔案句柄可能保持開啟。

在安全/運營上,正確使用 open() 的方式很簡單:

  • 先用 open() 取得格式、寬高等輕量資訊
  • 依政策拒絕:允許格式、最大解析度/像素數、上傳容量限制
  • 再進行驗證/解碼

簡言之,open() 應作為「在解碼前提取可判斷資訊」的工具。


verify() 保障什麼,保障什麼不是

Pillow 的 verify() 會嘗試確認檔案是否損毀,但不會實際解碼圖像資料。若發現問題會拋出例外,且在 verify() 後若要繼續使用圖像,必須重新開啟檔案。

安全觀點的結論有兩點:

  • 優點:避免重度解碼,快速過濾「損毀檔案」
  • 限制verify() 只表示「目前未顯著損毀」;若在 load() 時才暴露問題,仍可能存在風險。

load() 在驗證階段隨意呼叫會危險

load() 會真正執行解碼(包含解壓縮)並將像素載入記憶體。此時即成為 DoS(資源耗盡)攻擊面。即使檔案外觀小,解碼後可能膨脹至巨量。

Pillow 會以例外或警告處理「解壓縮風暴」風險,並設有預設閾值(如 128Mpx)作為保護。

Django 亦因同樣原因,在圖像上傳驗證時使用 verify() 而非 load()。程式碼中有「load() 會將整張圖載入記憶體,成為 DoS 向量」的註解,實際上會在 Image.open() 後呼叫 verify()


Django/DRF 使用時:ImageField 內部已呼叫 verify(),再呼叫可能重複

Django 的表單 ImageField 驗證會在內部執行 Image.open() + verify(),DRF 的 serializers.ImageField 亦同樣委派給 Django 進行驗證。

因此若已使用 DRF 的 serializers.ImageField

  • validate() 中再次呼叫 verify() 以「檢查損毀」通常是重複工作
  • 若需自訂商業驗證或額外安全檢查,建議改用 FileField(自行設計驗證流程),以明確掌握成本與責任。

為使用者上傳的檔案確保「安全」的實務做法

上傳檔案安全處理流程圖

最實際的答案是:

「不要直接使用上傳原始檔,讓伺服器解碼後再以新檔案儲存,並只使用這個新檔案」

  1. open() 讀取寬高/格式等低成本資訊,並以政策做一次性拒絕。
  2. verify() 過濾「明顯損毀」的檔案。
  3. 在受限環境下解碼,並將像素標準化為 RGB/RGBA 等通用格式。
  4. 伺服器以選定格式重新編碼,產生新檔。
  5. 服務僅儲存/提供伺服器重新編碼的檔案。

此策略的優點在於「伺服器掌控最終輸出」,能大幅移除原始檔中不必要的元資料或奇怪結構。

當然,重新編碼仍包含 load() 的解碼工作,故需先設定像素數/記憶體限制(防止解壓縮風暴),並盡量在工作者/隔離流程中執行,以確保安全。


總結

  • open()辨識 + 低成本資訊確認(像素可能尚未載入)
  • verify()損毀檔案的初步過濾(不解碼,使用前需重新開啟)
  • load()解碼/記憶體使用開始(驗證階段應避免頻繁呼叫)
  • 上傳安全實務:只信任伺服器重新編碼的結果(但必須先設置限制/隔離)

相關文章