檔案上傳把使用者可控制的二進位內容帶進應用、解析器、物件儲存與其他使用者的瀏覽器,是一條跨越多個信任邊界的資料供應鏈。安全設計不能只在前端限制副檔名,也不能把防毒掃描當成唯一答案;它需要先定義業務允許的格式,再以大小、名稱、媒體類型、檔案簽章、完整解析與內容檢查建立多層判定,將未通過檔案隔離,最後透過具授權的下載處理器安全交付。本文整理一套可以落地、監控與複驗的完整流程。

先講結論:安全上傳是一條受控管線,不是一個副檔名判斷式
使用者選擇檔案之後,內容可能依序經過瀏覽器、Reverse Proxy、應用伺服器、暫存區、格式解析器、防毒引擎、縮圖或文件預覽器、訊息佇列、物件儲存、CDN 與下載端點。任何一層把未驗證資料當成可信格式、可執行程式、檔案路徑或公開內容,都可能產生另一種弱點。
因此最可靠的模型不是『收進來再看看』,而是預設檔案不可信。接收階段只允許必要的業務格式與大小,產生內部 Object ID,立即寫入隔離區;後續工作在受限環境完成類型一致性、深度解析、惡意程式掃描、Content Disarm & Reconstruction(CDR)或重新編碼。只有全部 Gate 通過,狀態才轉為 APPROVED,並依使用者與物件關係提供存取。
這也代表拒絕上傳不是唯一成功條件。系統要能證明危險檔沒有被公開、沒有被 Web Server 執行、沒有在預覽器中觸發、沒有透過壓縮檔寫出隔離目錄,也沒有留下可被猜測的暫存網址。
- 先由業務定義必要格式,不接受『所有文件』或『所有圖片』這種模糊範圍
- 在讀取完整內容前限制 Request、檔案、使用者、租戶與時間窗配額
- 原始檔名、Content-Type 與副檔名視為未信任 Metadata,需正規化與交叉驗證
- 未完成檢查的檔案只存在私有 Quarantine,不可被公開 URL、預覽器或下游服務取得
- 通過後仍以物件層授權、正確 Content-Type、Content-Disposition 與 nosniff 安全交付
先畫資料流:上傳、處理、儲存與下載是不同信任邊界
上傳 Endpoint 只負責接收並建立待檢狀態,不應同步完成所有昂貴解析。掃描 Worker、轉檔服務與預覽器是高風險 Parser Boundary,應使用低權限、唯讀輸入、隔離網路、CPU/記憶體/時間限制與可丟棄執行環境;物件儲存則只依不可猜但仍需授權的內部 Key 保存內容。
下載是另一個安全決策。即使上傳者合法,也不代表任何知道 URL 的人都可讀取;即使檔案掃描通過,也不代表應直接以内嵌 HTML、SVG 或 PDF 在主站 Origin 執行。每一個邊界都要有明確輸入、狀態、身份、錯誤處置與稽核事件。
| 階段 | 主要輸入 | 必要控制 | 不得發生 |
|---|---|---|---|
| 接收 | Request、使用者、檔案 Metadata | 認證、授權、CSRF、大小與配額 | 未驗證內容進入 Webroot |
| 隔離 | 原始位元流與內部 Object ID | 私有儲存、不可執行、狀態鎖定 | 使用者可直接取得 URL |
| 解析/掃描 | 隔離物件 | 受限 Parser、AV、CDR、資源預算 | 高權限程序或開放出站網路 |
| 核准 | 所有 Gate 的一致結果 | 原子狀態切換與版本綁定 | 任一檢查未完成即放行 |
| 儲存 | 核准物件與安全 Metadata | 私有 Bucket、最小權限、加密與生命週期 | 以原始檔名當路徑 |
| 下載/預覽 | 使用者與 Object ID | 物件層授權、安全 Header、隔離 Origin | 以不可猜 URL 取代授權 |
威脅模型:同一個上傳功能可能影響伺服器、瀏覽器與其他系統
MITRE CWE-434 將問題描述為產品允許危險類型的檔案被自動處理。影響不只 Web Shell:檔案可能在伺服器端被執行、利用圖片/PDF/Office Parser 弱點、透過 HTML 或 SVG 對其他使用者造成 Stored XSS、覆寫檔案、消耗儲存與運算資源,或成為散布 Malware 與釣魚內容的公共 Hosting。
風險取決於系統會如何處理檔案。相同 PDF 在只提供 Attachment Download、以瀏覽器 Inline Preview、交給全文索引、擷取附件、轉圖或送進 AI Pipeline 時,攻擊面完全不同。安全需求必須從下游處理行為反推,而不是只看上傳表單接受什麼。
| 威脅 | 成立條件 | 可能影響 | 主要縱深控制 |
|---|---|---|---|
| 伺服器端執行 | 檔案進入可執行 Webroot 或被 Interpreter 處理 | RCE、資料外洩、橫向移動 | Webroot 外、不可執行儲存、類型 Allowlist |
| Stored XSS/主動內容 | HTML、SVG、XML 或錯誤 MIME 在主站 Origin 顯示 | Session/資料被竊、使用者操作遭冒用 | Attachment、nosniff、隔離 Origin、CSP Sandbox |
| Parser Exploit | 伺服器解析惡意圖片、PDF、Office 或媒體 | Worker 入侵、SSRF、檔案讀取 | Sandbox、更新 Library、最小權限、禁出站 |
| 路徑與覆寫 | 信任檔名或壓縮檔內 Path | 覆寫設定、靜態資源或其他使用者檔案 | 系統 Key、Path Containment、原子寫入 |
| 資源耗盡 | 大檔、批次、巨量像素、ZIP Bomb、巢狀封裝 | CPU、記憶體、磁碟與 Queue 不可用 | 多層 Size/Ratio/Count/Time Budget |
| 惡意內容散布 | 公開下載或分享功能缺乏治理 | Malware Hosting、品牌與法規風險 | AV/CDR、授權、檢舉、下架與稽核 |
第一步不是列黑名單,而是定義業務真的需要哪些格式
OWASP 建議以 Extension Allowlist 為起點,只接受業務必要且系統已準備安全處理的類型。『圖片』仍應明確到 JPEG、PNG 或 WebP;『文件』應區分 PDF、DOCX、XLSX 是否真的需要,以及要提供下載、預覽、索引還是轉檔。每多接受一種格式,就多一套 Parser、相容性與修補責任。
Denylist 很難列完所有危險副檔名、大小寫、替代 Handler、雙重副檔名與平台差異,最多只能作為額外告警。Allowlist 也不是單獨防線:合法的 JPEG、PDF 或 DOCX 仍可能帶惡意內容、超大尺寸、Parser Exploit 或外部連結,因此後續仍要深度驗證。
| 業務用途 | 可考慮格式 | 必要處理 | 預設不接受 |
|---|---|---|---|
| 個人頭像 | JPEG/PNG/WebP | 解碼、像素限制、重編碼、移除 Metadata | SVG、HTML、任意原圖格式 |
| 簽署文件 | 深度解析、AV/CDR、Attachment 或隔離預覽 | 可執行檔、巨集文件、壓縮包 | |
| 履歷附件 | PDF;確有需求才加 DOCX | AV、CDR、保存與下載授權 | HTML、RTF、含巨集格式 |
| 資料匯入 | 明確 CSV/JSON Schema | 編碼、欄位、列數、公式與大小限制 | 任意試算表或二進位檔 |
| 備份/套件 | 特定 Archive 或套件格式 | 隔離解壓、Path/Link/Count/Ratio 限制 | 未定義的巢狀封裝 |
五層類型驗證:每一層都只是證據,必須交叉一致
可靠判定會綜合正規化後副檔名、Client 宣告 Content-Type、Server 偵測 Media Type、檔案簽章與完整 Parser 結果。前兩項完全由使用者控制,適合快速拒絕誤傳,但不能作為安全信任;Magic Bytes 可辨識常見格式,卻可能被 Polyglot 或只在開頭偽造的內容繞過。
最有價值的判定是以預期 Library 完整解碼或解析,再確認結構、頁數、像素、內嵌物件、巨集、外部關聯與資源預算。若不同層不一致,例如 `.jpg`、`image/jpeg`,但 Parser 認出 ZIP/Office Container,應 Fail Closed 並保留最小必要證據。
| 層級 | 資料來源 | 能發現 | 不能單獨證明 |
|---|---|---|---|
| 副檔名 | 正規化後原始名稱 | 明顯非允許格式、使用者誤傳 | 真正內容與安全性 |
| Upload Content-Type | Multipart Header/Client | 一般格式錯誤 | 不可偽造的類型 |
| Server Media Detection | 檔頭與內容偵測 Library | 宣告與內容不一致 | 完整結構安全 |
| File Signature/Magic Bytes | 格式特徵 | 常見類型是否符合 | Polyglot、附加 Payload、Parser 行為 |
| 完整解析/重編碼 | 專用 Parser/Decoder | 結構、限制與可安全產出內容 | 所有未知 Parser 0-day 或 Malware |
副檔名檢查要先正規化:雙重副檔名只是其中一種繞過
檢查前先解析最後一個檔名元件,統一 Unicode 與大小寫策略,移除或拒絕控制字元、前後空白、尾端句點、替代路徑分隔符與平台保留名稱,再確認只有一個允許的最終 Extension。不同 OS、Web Server、Proxy 與 Framework 對 `file.php.jpg`、`file.jpg.php`、大小寫、Trailing Dot、Null Byte 或編碼字元可能有不同解讀。
不要嘗試靠複雜 Regex 猜完所有危險名稱。最安全做法是原始名稱只保存為已清理的顯示 Metadata,實際儲存 Key 由伺服器產生;類型與儲存副檔名則依 Server 完整解析出的核准格式建立,而不是沿用使用者字串。
| 檢查 | 風險 | 建議處置 | 證據 |
|---|---|---|---|
| 最後 Path Segment | `../`、反斜線與絕對路徑 | 不使用任何使用者目錄資訊 | 解析前後名稱 |
| Unicode/大小寫 | 相似字、正規化差異與 Case Bypass | 固定 Normalize 與 Case Policy | Canonical Name |
| 尾端字元 | Windows 尾端句點/空白與控制字元 | 拒絕或安全替換 | 拒絕原因代碼 |
| 多重副檔名 | 不同元件判斷不同 Extension | 以唯一核准最終格式重新命名 | 原始與儲存類型對照 |
| 平台保留名稱 | 裝置名稱、`.`、`..` 與 Shell 特殊值 | 永不作為實際儲存名稱 | 系統 Object ID |
Content-Type 由 Client 提供,不能視為可信類型
Multipart Upload 的 `Content-Type` 可由瀏覽器或呼叫者自行設定,攻擊者能把任意內容宣告為 `image/jpeg`。它仍有價值:可以快速拒絕一般誤傳,並與副檔名、Server Detection 與 Parser 結果比對;但任何安全放行都不能只依它。
回應端的 Content-Type 則由服務提供,直接影響瀏覽器如何解讀下載內容。RFC 9110 指出錯誤類型與 MIME Sniffing 可能帶來安全風險;系統應依核准後的實際格式產生單一正確 Content-Type,並以 `X-Content-Type-Options: nosniff` 降低瀏覽器自行改判主動內容的風險。
Magic Bytes 與 Polyglot:檔頭符合仍不代表整份檔案安全
許多格式在開頭或固定位置有 Signature,能協助辨識 JPEG、PNG、PDF、ZIP 等內容。這比信任 Header 更可靠,但只檢查前幾個 Byte 仍可能接受『開頭像圖片、後面含另一種格式』的 Polyglot,或接受結構損壞、內嵌主動內容與超大資源消耗的合法容器。
應以專用 Parser 完整走訪結構,拒絕多餘尾端資料、異常 Chunk、重複 Header、超出規格的尺寸與未知關聯;是否拒絕某種合法但未使用的 Feature,取決於業務功能與 Parser 能力。Signature 驗證是必要的快速 Gate,不是 AV 或完整解析的替代品。
完整解析與重編碼:不要把未知位元原封不動交給下一層
對圖片,可用受維護的 Decoder 真正解碼像素,再以固定格式、品質、尺寸與 Metadata Policy 重新編碼,產生新的安全衍生檔;對文件,可檢查容器結構、巨集、內嵌物件、外部關聯與頁數,再依需求使用 CDR 移除不必要主動元素。使用者原始檔可保留在受限證據區,實際交付的是核准後產物。
Parser 本身也是攻擊面。它應在隔離 Worker 執行,使用最新安全版本,限制 CPU、記憶體、檔案描述符、處理時間、暫存空間與網路;錯誤時 Fail Closed,不能因 Thumbnail 或 Preview 失敗就回退成直接提供原始內容。
| 控制 | 目的 | 建議基線 | 失敗處置 |
|---|---|---|---|
| 低權限身分 | 限制 Parser Exploit 後能力 | 無 Shell、無 Secret、唯讀輸入、專用輸出 | 銷毀 Worker |
| 網路隔離 | 阻斷 SSRF、外部 Fetch 與 C2 | 預設無出站,必要目的地 Allowlist | 中止並告警 |
| 資源限制 | 避免大像素、深層物件與惡意壓縮耗盡 | CPU/RAM/Time/Disk/File Count Budget | 標記 Resource Limit |
| 格式專用 Parser | 避免通用工具錯誤猜型 | 只解析核准格式與必要 Feature | 型別不一致即拒絕 |
| 安全輸出 | 切斷原始 Payload 與主動功能 | 新 Object ID、固定編碼、最小 Metadata | 不發布部分產物 |
檔名與路徑:原始 Filename 只能是顯示資料
RFC 6266 明確提醒,Content-Disposition 的 Filename 應被視為建議值,接收端不可因此寫入未授權位置;實作上同樣適用上傳。伺服器應產生 UUID、ULID 或其他內部 Object ID 作為 Key,Original Filename 經長度、Unicode、控制字元與顯示安全處理後,只存放在資料庫 Metadata。
路徑由可信設定與內部 ID 組合,寫入前做 Canonicalization 並確認仍位於預期根目錄。不要把使用者 ID、Tenant ID、原始名稱或 Query Parameter 直接串成 Filesystem Path;也不要假設 `basename()` 足以處理不同編碼、分隔符、Symlink 與競態。物件儲存同樣要避免讓呼叫者任意指定跨租戶 Prefix 或覆寫既有 Key。
| 欄位 | 來源 | 用途 | 安全規則 |
|---|---|---|---|
| Original Filename | 使用者 | UI 顯示與下載建議名稱 | 清理、限長、不可作 Path 或型別依據 |
| Object ID/Key | Server | 唯一儲存位置 | 高熵、不可覆寫、綁定 Tenant 與 Owner Metadata |
| Detected Media Type | Server Parser | 處理與安全回應 | 只取核准結果,不信任 Client |
| Content Hash | Server | 完整性、去重或調查關聯 | 不是 Malware 安全證明 |
| Display Filename | Server 清理後 | Content-Disposition | 同時提供安全 ASCII Fallback 與 UTF-8 filename* |
大小、數量與配額:越早拒絕,越少資源被消耗
只在應用收到完整檔案後檢查 Size,攻擊者已消耗 Reverse Proxy、頻寬、暫存磁碟與 Worker。限制應存在於 CDN/Gateway、Web Server、Framework Multipart Parser、應用業務規則、Object Storage Policy 與非同步 Queue;各層值要一致,錯誤回應也不能洩漏內部路徑或允許無限重試。
除了單檔大小,還要限制 Request 總量、檔案數、每日使用者/租戶配額、並行 Multipart Upload、未完成 Upload、轉檔輸出與保存天數。圖片看似只有數 MB,解碼成巨大像素矩陣後仍可能耗盡記憶體,因此要另外限制 Width、Height 與總 Pixel Count。
| 位置 | 限制項目 | 防止 | 監控 |
|---|---|---|---|
| CDN/Gateway | Request Body、速率、連線時間 | 頻寬與入口資源耗盡 | 拒絕量、來源與狀態碼 |
| Web/Framework | Multipart Parts、Header、Temp File | Parser 與暫存磁碟耗盡 | Temp 使用量與解析錯誤 |
| Application | 業務單檔、檔數、User/Tenant Quota | 濫用與成本失控 | 配額使用與異常成長 |
| Processing Worker | CPU、RAM、Timeout、Output Size | Parser DoS 與轉檔放大 | 超限原因與格式 |
| Object Storage | Prefix、Lifecycle、Multipart Cleanup | 孤兒物件與長期成本 | 未完成 Upload 與儲存成長 |
壓縮檔與 ZIP Bomb:限制解壓後大小、檔數、比例與路徑
CWE-409 描述高壓縮輸入在解壓後產生巨大資料的 Data Amplification。單純限制 ZIP 本身大小無法阻擋;應在真正寫出前檢查宣告與實際的 Uncompressed Size、Compression Ratio、Entry Count、Nested Archive Depth、單 Entry 大小與總處理時間,並以串流方式持續累計,超限立即中止。
每個 Entry Path 都要正規化,拒絕絕對路徑、`..`、替代分隔符、Symlink、Hardlink、Device File 與任何逃出專用 Extraction Root 的結果。解壓在一次性 Sandbox 執行,輸出只透過安全 API 搬到下一階段;不得直接把 Archive 解到應用目錄、Webroot 或既有資料夾。
| Gate | 必要限制 | 阻擋風險 | 驗證方式 |
|---|---|---|---|
| 總解壓大小 | 宣告值與實際串流雙重上限 | ZIP Bomb/磁碟耗盡 | 超限中止且不保留部分輸出 |
| Compression Ratio | 每 Entry 與整體比例上限 | 高倍 Data Amplification | 小輸入大輸出測試 |
| Entry Count/Depth | 檔數與巢狀 Archive 層數 | Inode、CPU 與遞迴耗盡 | 大量小檔與巢狀測試 |
| Path Containment | Canonical Path 必須留在 Extraction Root | Zip Slip/任意覆寫 | `../`、絕對路徑與混合分隔符 |
| Link/特殊檔 | 拒絕 Symlink、Hardlink、Device 與 FIFO | 跳出隔離或特殊裝置操作 | 各 Archive 格式專項案例 |
| 執行環境 | 無網路、低權限、限制 CPU/RAM/Time | Parser Exploit 與資源耗盡 | Sandbox Policy 與超時證據 |
圖片上傳:副檔名正確,仍要處理像素、Metadata、SVG 與 Decoder
圖片應以實際 Decoder 完整解碼,限制 Width、Height、總 Pixel Count、Frame Count、色彩資料與輸出大小,再重編碼成業務允許的固定格式。重編碼能移除多餘 Chunk 與部分附加內容,但 Parser 必須在受限環境執行,且不能因解碼失敗就回傳原圖。
EXIF 可能包含 GPS、裝置與拍攝時間,應依隱私需求移除。SVG 是 XML 與主動文件格式,不應因為『看起來是圖片』就與 JPEG 同等處理;若沒有完整 Sanitization 與隔離渲染需求,最簡單安全策略是不接受使用者 SVG,或轉成不含主動內容的 Raster Image。動畫格式也要限制 Frame 與總解碼成本。
| 面向 | 風險 | 控制 | 核准產物 |
|---|---|---|---|
| 尺寸/像素 | 小檔解碼成巨量記憶體 | Width、Height、Pixel、Frame 上限 | 符合尺寸的重編碼圖 |
| EXIF/Metadata | 位置、裝置與個資外洩 | 依用途 Strip 或 Allowlist | 最小 Metadata |
| SVG | Script、外部資源、Event 與 XML 攻擊面 | 預設拒絕;必要時專用 Sanitizer/Rasterize | 隔離後 Raster 或嚴格安全 SVG |
| Decoder | 惡意結構利用 Library 弱點 | 更新版本、Sandbox、資源限制 | 由受控 Worker 產生 |
| Thumbnail | 縮圖服務成為額外 Parser Boundary | 與原檔同等隔離與授權 | 新 Object ID 的衍生檔 |
PDF、Office 與其他複合文件:預覽之前先處理主動內容
PDF、DOCX、XLSX 與 PPTX 可能包含 JavaScript、巨集、內嵌檔案、外部連結、OLE Object、表單、媒體與多層壓縮。只確認 ZIP Container 或 `%PDF` Header,無法判斷這些 Feature 是否符合業務需求。應使用格式專用 Analyzer,建立可允許 Feature 清單,並以 AV、Sandbox 或 CDR 補充。
預覽器通常比下載端點更危險,因為它會主動解析與渲染。可將預覽服務放在無使用者 Session Cookie 的獨立 Origin,以 Sandbox、嚴格 CSP、禁網路與短生命週期 Worker 運行;若預覽失敗,不要回退成在主站 Inline 顯示原始檔。
| 處理需求 | 建議方式 | 必查 Feature | 安全失敗 |
|---|---|---|---|
| 只需保存/下載 | AV+私有儲存+Attachment | 類型、大小、Malware、授權 | 拒絕或維持隔離 |
| 需要預覽 | 隔離 Renderer 產生 Raster/安全衍生檔 | Script、External Link、Embedded File、資源 | 不提供原始 Inline Fallback |
| 需要內容索引 | Sandbox Extractor 輸出純文字 | Parser、外部關聯、編碼與大小 | 不索引且保留待審狀態 |
| 需要可編輯 | CDR 或受控 SaaS/Editor 邊界 | Macro、OLE、Formula、外部資料 | 明確告知不支援 Feature |
惡意程式掃描、Sandbox 與 CDR:三者解決的問題不同
Antivirus 適合辨識已知 Malware、可疑特徵與部分啟發式行為,但可能有偵測延遲、Encrypted Archive、格式支援與 False Positive/False Negative;Sandbox 可觀察文件或程式在受控環境的行為,但成本與時間較高;CDR 則以解析並重建內容移除不必要主動元素,適合 PDF、Office 等可安全重建的格式。
三者都不是萬能。掃描服務要記錄 Engine、Signature/Model Version、時間與結果;失敗、Timeout、引擎不可用或結果不確定時,檔案維持 Quarantine,而不是 Fail Open。若檔案含個資、機密或客戶資料,不得未經授權送往公共掃描服務,Hash 查詢也要評估資訊洩漏。
| 控制 | 主要價值 | 限制 | 建議用法 |
|---|---|---|---|
| Antivirus | 已知 Malware 與部分啟發式偵測 | 未知樣本、加密內容、版本延遲 | 作為必經 Gate,記錄 Engine Version |
| Sandbox | 觀察開啟/執行後行為 | 成本、時間、Evasion 與資料敏感性 | 高風險格式或事件升級 |
| CDR | 移除巨集、Script、Embedded Object 等主動 Feature | 格式支援與功能損失 | 業務可接受安全重建時使用 |
| 重新編碼 | 產生最小、可控的圖片/媒體輸出 | 依賴 Decoder 安全 | 在受限 Worker 產生新物件 |
| Hash Reputation | 快速辨識已知樣本 | Hash 未知不代表安全,可能洩漏存在性 | 私有資料庫優先、只作補充 |
Quarantine 狀態機:掃描完成前,檔案不能被任何正式流程使用
非同步處理需要明確 State Machine,例如 `UPLOADING → QUARANTINED → SCANNING → APPROVED/REJECTED/ERROR`。狀態存在可信資料庫,Object Version/Hash 與掃描結果綁定;任何內容改變都產生新版本並重新檢查,不能沿用舊檔的 APPROVED 狀態。
這能避免 Time-of-check to Time-of-use(TOCTOU):掃描的是 A,最後發布的卻是同 Key 被覆寫後的 B。掃描 Worker 以唯讀方式取得不可變版本,核准時以條件式或原子操作確認 Hash/Version 未變;下載、預覽、索引、分享與 Webhook 都只接受 APPROVED。
| 狀態 | 允許操作 | 禁止操作 | 轉移條件 |
|---|---|---|---|
| UPLOADING | 寫入指定暫存 Object Version | 讀取、分享、解析下游 | 完整性與大小確認 |
| QUARANTINED | 內部 Scanner 讀取 | 使用者下載、公開 URL、正式索引 | 所有必要 Job 已建立 |
| SCANNING | 受限 Worker 解析/AV/CDR | 覆寫同版本、提前發布 | 全部 Gate 得到一致結果 |
| APPROVED | 依授權下載、預覽與下游處理 | 繞過物件層授權 | Hash/Version 與結果綁定 |
| REJECTED | 保留最小調查證據或刪除 | 公開、重試同內容繞過 Policy | 依 Retention/Incident Policy |
| ERROR | 受控重試、人工審查 | Fail Open | 引擎恢復或明確風險決策 |
儲存位置:Webroot 外、私有、不可執行,並與應用權限分離
OWASP 的安全優先順序是使用不同主機或服務保存上傳檔,其次是 Webroot 外;若不得不進 Webroot,仍要確保不可執行並透過嚴格存取控制。現代架構通常使用私有 Object Storage Bucket/Container,關閉 Public ACL,以不同 IAM Role 分離 Upload、Scanner、Publisher 與 Download Handler。
『Bucket 是私有的』只是起點。應用 Role 不應擁有整個 Bucket 的任意 List/Read/Delete;Scanner 只讀 Quarantine Prefix、只寫受控結果;Publisher 使用條件式 Copy 或狀態更新;下載服務只在通過物件層授權後取得單一 Approved Object。Server-side Encryption、Versioning、Object Lock 與備份則依資料分類與復原需求設計。
| 角色 | 需要權限 | 不應擁有 | 稽核事件 |
|---|---|---|---|
| Upload Service | 建立專用 Quarantine Object/Multipart | 讀取他人檔案、發布、刪除歷史 | Object ID、Owner、Checksum、Size |
| Scanner/Parser | 唯讀指定不可變版本、寫結果/衍生檔 | 公開 ACL、修改原檔、任意出站 | Engine、結果、資源與時間 |
| Publisher | 將核准版本標記可用或安全搬移 | 跳過結果、任意覆寫 | Old/New State 與 Version |
| Download Handler | 讀取已授權的 Approved Object | List Bucket、讀 Quarantine | User、Tenant、Object、用途 |
| Lifecycle Job | 依政策刪除孤兒與到期物件 | 讀取內容或改變 Owner | 批次範圍、數量、錯誤 |
物件層授權:知道 URL 或 Object ID,不能等同有權下載
下載與刪除必須從已驗證 Session 取得 User/Tenant,再查詢 Object Ownership、角色、分享狀態與業務規則。Object ID 使用高熵值可降低猜測,但不是 Authorization;任何直接以 `/files/{id}`、Storage Key、縮圖 ID 或 Export Job ID 取得內容的端點,都要逐請求驗證。
同一檔案的原圖、縮圖、預覽、轉檔與備份衍生物需繼承一致 Policy。常見錯誤是原檔有授權,Thumbnail CDN、Signed Preview URL 或背景 Export 卻只靠不可猜名稱。跨租戶交換 Object ID、使用已停用帳號、撤銷分享後重播 URL,都應納入負向測試。
Direct Upload 與 Presigned URL:它是 Bearer Token,不是掃描通行證
Browser 直接上傳到 Object Storage 可降低應用 Server 負載,但仍要先由應用驗證使用者與配額,產生只能寫入單一隨機 Quarantine Key、限定 Method、短有效期與必要 Header/Policy 的 Signed Request。AWS 明確提醒 Presigned URL 具有 Bearer Token 性質,而且同 Key 已存在時 PUT 可能覆寫;因此 Key 必須唯一且不可重用。
上傳完成後,Client 回報不能直接把狀態改成 APPROVED。Server 應透過可信 Storage Event 或 Head/Metadata 取得實際 Size、Checksum、Version 與 Key,再建立掃描任務。Checksum 可以驗證傳輸完整性,不能證明內容沒有 Malware。Multipart Upload 的每個 Part、Complete 動作、逾期 Session 與未完成 Part 都要受控並定期清理。
| 項目 | 安全設定 | 防止 | 完成後驗證 |
|---|---|---|---|
| Object Key | Server 產生單一 Quarantine Key | 跨租戶寫入與覆寫 | 事件 Key 與預期完全一致 |
| Method/Operation | 只允許必要 PUT/POST/Multipart 動作 | 權限擴張與任意讀刪 | 實際 Request Method |
| 有效期 | 完成上傳所需的最短窗口 | URL 洩漏後長期濫用 | Expired/Replay 測試 |
| Size/Type Policy | 可簽入 POST Policy 或在入口再次限制 | 過大與明顯錯型 | 實際 Size 與 Server Detection |
| Checksum | 簽入並由 Storage 驗證 | 傳輸損壞與內容替換 | Hash/Version 綁定掃描 |
| Finalize | Server 信任 Storage 狀態而非 Client 宣告 | 跳過上傳或偽造完成 | Head/Event/Version 與 Scan State |
下載回應:正確 Content-Type、Content-Disposition 與 nosniff
下載服務依 Server 核准的 Media Type 設定 Content-Type,不沿用 Upload Header。對不需要在瀏覽器中執行的內容使用 `Content-Disposition: attachment`;Filename 只使用清理後顯示值,並依 RFC 6266 同時提供安全 Fallback 與 UTF-8 `filename*`。`X-Content-Type-Options: nosniff` 可降低瀏覽器把內容改判為 Script 或其他主動類型。
對高風險或未知格式可使用 `application/octet-stream` 強制下載,但這不會消除使用者開啟檔案後的 Malware 風險,也不會取代掃描與授權。回應還要考慮 Cache-Control、內容長度、Range、Referrer 與 CDN Key,避免私人檔案被共享快取或在 URL、Log 與 Referer 洩漏 Signed Token。
| Header/行為 | 建議 | 主要目的 | 常見錯誤 |
|---|---|---|---|
| Content-Type | 使用 Server 核准的單一實際類型 | 讓 Client 正確、安全解讀 | 沿用使用者宣告或重複 Header |
| Content-Disposition | 非必要 Inline 時使用 attachment | 降低主動內容在 Origin 執行 | 所有上傳內容直接 inline |
| filename/filename* | 清理、限長、無 Path,UTF-8 安全編碼 | 避免路徑與 UI 混淆 | 回傳原始未處理 Filename |
| X-Content-Type-Options | nosniff | 降低 MIME Sniffing | 以為它能取代正確 Content-Type |
| Cache-Control | 依資料分類設 private/no-store 或短效策略 | 避免共享 Cache 洩漏 | 私人內容使用 public cache |
| 授權 | 每請求或極短 Signed URL 綁定單一物件 | 阻擋 IDOR/BOLA | 只靠不可猜 URL |
Inline Preview:使用獨立 Origin、Sandbox 與最小權限
若業務需要在 Browser 內預覽 PDF、圖片或其他內容,優先顯示由隔離 Renderer 產生的安全衍生物,而不是原始檔。預覽 Origin 不應帶主站 Session Cookie,使用獨立 Hostname、嚴格 CSP、Frame Sandbox、無任意 Script 與受控 CORS;主站透過短效、單物件授權取得內容。
即使使用 `sandbox` Iframe,也要依實際功能審查允許項目;同時給予 `allow-scripts` 與 `allow-same-origin` 可能削弱隔離。預覽服務不得存取內網、Cloud Metadata 或主站 Secret,因為惡意文件可能觸發外部資源與 Parser Request。
認證、CSRF、Rate Limit 與批次上傳:入口控制不能被旁路
只有已授權角色能上傳到指定資產或租戶。使用 Cookie Session 的 Endpoint 要有 CSRF 防護;API Token 要限制 Scope;所有入口包含 REST、GraphQL、Mobile API、Admin Import、Editor Paste、Email Ingestion 與 Direct Upload Finalize 都要套用同一 Policy。前端 `accept` 屬性只改善使用體驗,不能取代 Server Validation。
Rate Limit 與配額應同時按 User、Tenant、IP、Object Count、Byte 與處理成本計算。Batch Upload 不能只檢查第一個檔案;部分成功策略、回滾、單檔狀態與總配額要明確。重試要使用 Idempotency Key 或不可變 Upload ID,避免同一內容重複建立與掃描。
| 入口 | 身分/意圖 | 內容控制 | 常見旁路 |
|---|---|---|---|
| Web Form | Session+CSRF+角色 | 完整 Server Pipeline | 只靠 accept/JavaScript |
| REST/GraphQL | Token Scope+Object Authorization | 每個 Part/Variable 檢查 | 替代 Endpoint 未套 Middleware |
| Direct Storage | 短效 Presigned Request | Quarantine Key/Policy/Finalize | Client 直接宣告 APPROVED |
| Admin Import | 高權限重驗+操作範圍 | 相同或更嚴格 Parser Policy | 因為是 Admin 就完全信任 |
| Email/Webhook Ingestion | 可信來源與 Tenant Mapping | 附件照常隔離與掃描 | 繞過一般 Upload Service |
| Batch/Archive | 總配額+逐項授權 | 每項類型與解壓 Gate | 只檢查外層或第一個檔 |
Metadata、隱私與加密:不要讓安全處理製造新的資料外洩
檔案本體、Original Filename、EXIF、Author、Document Property、AV 結果、OCR 文字、Preview 與 Search Index 都可能包含個資或機密。資料分類應決定誰可讀、是否允許外部掃描、保存多久、是否可跨區域處理、是否需要 Customer-managed Key,以及 Log 能保留哪些欄位。
Encryption at Rest 與 TLS 是必要控制,但不會修復錯誤授權。Key Policy 要與 Storage Role 分離,備份、Replica、縮圖與暫存同樣納入。掃描服務只取得必要期間的存取權,不在一般 Job Log 印出完整 Key、Signed URL、Filename、內容片段或 Token。
保留、刪除與孤兒清理:拒絕的檔案也有生命週期
QUARANTINED、REJECTED、ERROR、未完成 Multipart、失敗衍生檔與舊 Version 若沒有 Lifecycle,會長期累積敏感與惡意內容。政策應定義每種狀態保存期限、調查 Hold、刪除方式、Backup/Replica 延遲與誰能覆核;一般營運人員不應能隨意下載拒絕樣本。
使用者刪除帳號、撤回分享、權限變更與法規請求時,要同步處理原檔、衍生物、索引、快取、Signed URL 能力與事件紀錄。若必須保留稽核證據,保留的是最小 Metadata、Hash 與結果,而不是無期限保存完整危險檔。
| 資料類型 | 保存原則 | 刪除觸發 | 仍需保留 |
|---|---|---|---|
| 未完成 Multipart | 短時間窗口 | 逾時或 Upload Session 取消 | Upload ID、Owner、Byte 與原因 |
| QUARANTINED/ERROR | 足以重試或調查的最短期限 | Job 結束或期限到 | Hash、Engine、錯誤與時間 |
| REJECTED | 依 Incident/法遵需要限權保存 | 無調查需求即刪除 | 最小判定證據 |
| APPROVED | 依業務與資料分類 | 使用者刪除、合約/政策到期 | 必要 Audit Record |
| Preview/Thumbnail | 不得超過原檔生命週期 | 原檔刪除或權限撤銷 | 衍生關係與刪除結果 |
Log 與監控:要能回答誰上傳、掃描什麼、何時被取用
每個 Upload 使用唯一 Correlation ID,串起 Session/Token Subject、Tenant、Object ID、Version、Size、Detected Type、Parser、AV/CDR Version、State Transition、下載與刪除。Log 不保留可重播 Signed URL、完整 Credential、原始檔案內容或不必要的個資。
監控要涵蓋拒絕率突增、同來源大量上傳、Size/Ratio/Parser Timeout、AV Engine 不可用、Quarantine 積壓、APPROVED 前讀取嘗試、跨租戶授權拒絕、公開 ACL 變更、Signed URL 異常使用與儲存成本。告警需能定位 Owner 與立即停止發布。
| 訊號 | 可能問題 | 優先確認 | 自動處置 |
|---|---|---|---|
| 拒絕/未知類型突增 | 攻擊、誤用或 Client 版本問題 | 來源、Tenant、檔頭與 Release | 限流並通知 Owner |
| Parser Timeout/Crash | 惡意樣本、資源限制或 Library 弱點 | 格式、Hash、Worker Version | 隔離樣本並銷毀 Worker |
| Quarantine 積壓 | Scanner 故障或容量不足 | Engine Health、Queue Age、容量 | 停止新放行、擴容或降載 |
| APPROVED 前讀取 | 授權或狀態檢查缺失 | Endpoint、Caller、Object State | 阻擋並建立 Incident |
| 公開 ACL/Bucket Policy 變更 | 配置 Drift 或誤操作 | Change Actor 與受影響 Prefix | 自動回復安全基線 |
| Signed URL 異常 | 外洩、重播或過長有效期 | Signature Age、來源與 Object | 撤銷底層 Credential/物件能力 |
怎麼測試檔案上傳?以安全樣本建立正向與負向矩陣
測試前取得書面授權,使用專用帳號、租戶、Storage Prefix 與低流量窗口。以最小安全樣本驗證每層行為:正確格式、錯誤副檔名、偽造 Content-Type、簽章不符、損壞結構、超出尺寸/像素/Entry Count、Path Traversal Entry、SVG 主動內容、跨租戶 Object ID 與過期 Signed URL。不要在正式環境散布真實 Malware 或可執行 Web Shell。
OWASP WSTG 提到可用 EICAR 測試檔驗證 Anti-malware 流程;即使如此,也要先與 SOC、AV Owner、Storage Owner 核准,避免觸發正式事件或被其他系統擴散。測試證據要確認最終狀態、儲存位置、下游未執行、下載被拒絕與 Log/Alert 正確,不只看 API 回傳 400。
| 測試面 | 安全案例 | 預期結果 | 必要證據 |
|---|---|---|---|
| Allowlist | 未允許與雙重副檔名 | 接收前或 Quarantine 內拒絕 | Canonical Name 與原因 |
| 類型一致性 | Header/Extension/Signature/Parser 不一致 | Fail Closed,不建立 APPROVED | 各層偵測結果 |
| 資源 | 過大、巨量像素、ZIP Ratio/Count/Depth | 早期中止且部分輸出清除 | 限制值與實際值 |
| 內容安全 | 核准的 EICAR/測試標記或 CDR 測試文件 | Engine 偵測、隔離、無下載 | Engine Version、State、Alert |
| 路徑 | `../`、絕對路徑、Symlink 與混合分隔符 | 未寫出 Extraction Root | Sandbox File Inventory |
| 授權 | 跨使用者/租戶 Object、Preview 與衍生檔 | 一致拒絕且無內容洩漏 | Account/Object Matrix |
| Signed URL | 過期、改 Key、改 Method、重播與覆寫 | 簽章或 Policy 拒絕 | Request 與 Storage Event |
| 下載 | HTML/SVG/未知內容 | 安全 Attachment/隔離預覽、nosniff | Response Header 與瀏覽器行為 |
安全檔案上傳的 10 階段標準流程
這套流程把內容判定、隔離、儲存與交付拆成可稽核的 Gate。主路徑只讓符合業務格式、資源限制、完整解析與內容安全檢查的不可變版本前進;任何未知、錯誤、Timeout 或引擎不可用都停留在 Quarantine,不以可用性理由自動放行。
| 階段 | 核心工作 | 必要 Gate | 交付物 |
|---|---|---|---|
| 01 身分與範圍 | 認證、CSRF、角色、Tenant 與用途 | Caller 有權建立該類物件 | Upload Authorization |
| 02 資源預算 | Request、File、Count、Quota、Timeout | 入口層即限制 | Resource Budget |
| 03 名稱分離 | 清理顯示名稱、Server 產生 Object Key | 使用者字串不作 Path | Object Metadata |
| 04 類型驗證 | Extension、MIME、Signature、Parser 交叉一致 | 只接受業務 Allowlist | Type Evidence |
| 05 隔離解析 | 專用 Parser、Sandbox、重編碼/CDR | 無出站且受資源限制 | Parsed Artifact |
| 06 內容安全 | AV、Sandbox、主動內容與格式 Feature | 未知/Timeout 不放行 | Scan Evidence |
| 07 Archive Gate | Size、Ratio、Count、Depth、Path、Link | 輸出留在隔離根目錄 | Extraction Manifest |
| 08 Quarantine 決策 | 結果綁定 Hash/Version,原子轉狀態 | 所有必要 Gate 通過 | Approval Record |
| 09 私有儲存 | 不可執行 Object Store、最小 IAM、Owner Mapping | Public ACL 關閉 | Storage Policy |
| 10 安全交付 | 物件授權、短效能力、正確 Header、Audit/Delete | 只有 APPROVED 可讀 | Download/Lifecycle Evidence |

修復與複驗:確認危險內容沒有在另一條路徑被使用
修復先定位 Root Cause:是 Allowlist 未落在 Server、檔名直接形成 Path、Quarantine 可公開讀、Scanner Fail Open、同 Key 可覆寫、預覽器直接開原檔,還是下載缺少授權。只新增一條 Extension Regex,無法關閉後段的儲存、解析與交付風險。
複驗要保留原始負向樣本與 Account/Object Matrix,先確認合法檔仍可完整上傳、處理與下載,再確認每個旁路都拒絕;同時查 Storage、Queue、Preview、CDN、Log 與衍生檔,證明沒有部分產物、公開 URL 或背景 Job。若 Library 或 AV Rule 更新,應對既有 Quarantine/Approved 物件依風險重新掃描。
| 修復面 | 正向案例 | 負向案例 | 關閉證據 |
|---|---|---|---|
| 類型 | 核准格式完整處理 | 偽造 Header/Extension/Signature 不一致 | Parser 結果與 Reject State |
| 路徑/Key | 新 Object 唯一且可追蹤 | Traversal、同 Key 覆寫、跨 Prefix | Storage Event 與無越界寫入 |
| 內容安全 | 乾淨檔轉 APPROVED | 測試 Malware/主動內容/Timeout | Engine Version 與 Fail Closed |
| 資源 | 邊界內檔案完成 | Size/Pixel/ZIP/Batch 超限 | 早期拒絕且 Temp 清除 |
| 授權 | Owner 可下載核准物件 | 他人、他租戶、撤銷分享與 Quarantine | 所有衍生路徑一致拒絕 |
| 下載 | 正確格式與檔名 | HTML/SVG/未知格式與 MIME Sniff | Header、隔離 Origin 與瀏覽器結果 |
常見誤區:看起來有檢查,不代表形成安全閉環
上傳安全最常失敗在各元件解讀不一致:前端看 Extension、API 看 Content-Type、Storage 信任 Original Filename、Web Server 依另一個 Extension 執行、Browser 又對回應做 MIME Sniff。真正安全的流程必須由 Server 產生單一核准類型與狀態,並讓所有下游只接受同一份可信結果。
| 誤區 | 為什麼不夠 | 正確作法 |
|---|---|---|
| 前端 `accept` 已限制 | Request 可被直接修改 | Server Allowlist 與全流程驗證 |
| 只檢查副檔名 | 可偽造、雙重名稱、平台解讀不同 | 正規化+類型交叉一致+完整解析 |
| Content-Type 是 image/jpeg | 完全由 Client 宣告 | Server Detection、Signature 與 Decoder |
| Magic Bytes 符合就安全 | 可能是 Polyglot、異常結構或惡意合法格式 | 完整解析、重編碼、AV/CDR |
| 防毒沒報就安全 | 未知樣本、解析器與主動內容仍存在 | 多層 Gate、Sandbox 與最小 Feature |
| UUID 很難猜,不用授權 | 不可猜不等於合法存取 | 每請求 Object/Tenant Authorization |
| 私有 Bucket 就完成 | 應用 Role、Signed URL、Preview 與衍生檔仍可能外洩 | 最小 IAM、短效能力與全路徑稽核 |
| 掃描失敗先放行 | 把防護不可用轉成攻擊旁路 | 維持 Quarantine、受控重試與告警 |
上線前檢查清單
以下項目應綁定實際 Endpoint、Storage、Parser、角色與證據。每個未完成項目都要有 Owner、補償控制、到期日與複驗條件;不要用『已有防毒』概括檔案類型、路徑、資源、授權與下載風險。
- 每個上傳用途都有明確格式、大小、數量、像素、頁數、Archive 與 Feature Allowlist
- 所有入口包含 Web、API、Mobile、Admin、Email、Webhook 與 Direct Upload 共用同一政策
- 認證、CSRF、角色、Tenant、Rate Limit、Quota 與 Idempotency 已在 Server 驗證
- Extension、Client Content-Type、Server Detection、Magic Bytes 與完整 Parser 結果必須一致
- 原始 Filename 只作已清理顯示 Metadata;實際 Object Key 由 Server 產生且不可覆寫
- 圖片、PDF、Office、SVG、Archive 與其他核准格式都有專用 Parser、Feature 與資源限制
- AV、Sandbox、CDR 或重編碼依格式使用;Engine Error/Timeout 時維持 Quarantine
- Archive 限制 Uncompressed Size、Ratio、Entry Count、Depth、Path、Symlink 與處理時間
- Quarantine 私有、不可執行,APPROVED 前不可下載、預覽、索引、分享或進入下游
- Storage Role 採最小權限,Public ACL 關閉,原檔、衍生物與各 Version 權限一致
- Signed Upload/Download 限單一 Object、Method、短效時間並視為需保護的 Bearer Token
- 下載使用核准 Content-Type、清理後 Content-Disposition、nosniff 與資料分類 Cache Policy
- Log、監控、Retention、刪除、Multipart Cleanup、Incident 與修復複驗流程均已演練
Redmark 如何協助檢查檔案上傳與公開攻擊面
Redmark 協助團隊將網站資產、登入後流程、掃描任務、風險證據與修復追蹤集中管理。規劃檔案上傳檢測時,團隊應先確認合法授權、允許格式、專用測試帳號與不含真實個資的安全樣本,再依實際 Endpoint、角色、租戶、儲存與下載路徑建立測試矩陣。
自動化掃描能發現公開 Endpoint、類型與回應配置、常見旁路與部分內容處理風險,但 Parser Sandbox、Private Storage IAM、AV/CDR Pipeline 與 Retention 仍需要系統 Owner 提供架構證據。最佳做法是把外部掃描、內部配置查核與原始負向案例的修復複驗整合成同一個持續流程。
資料來源
- OWASP File Upload Cheat Sheet↗
- OWASP WSTG — Test Upload of Unexpected File Types↗
- OWASP WSTG — Test Upload of Malicious Files↗
- MITRE CWE-434 — Unrestricted Upload of File with Dangerous Type↗
- MITRE CWE-409 — Improper Handling of Highly Compressed Data↗
- MITRE CWE-22 — Path Traversal↗
- IETF RFC 6266 — Content-Disposition in HTTP↗
- IETF RFC 9110 — HTTP Semantics↗
- AWS S3 — Download and Upload Objects with Presigned URLs↗


