題目與適用情境
一個多租戶 B2B 文件服務允許已登入使用者上傳 PDF、JPEG 和 PNG 附件,單一檔案上限為 20 MiB。上傳完成後,只有同租戶內具備權限的成員可以下載。用戶端提交的檔案位元組、原始檔名、副檔名、Content-Type 和檔案大小宣告都屬於不可信任輸入。
請設計一套安全的上傳、掃描、發布與下載 API,並說明如何防禦:
- Web shell、偽造類型、雙副檔名、路徑穿越和覆寫既有物件;
- 惡意 PDF、圖片解析器漏洞、病毒和多格式檔案;
- 跨租戶越權、公開物件位址洩漏和危險的瀏覽器行內呈現;
- 超大請求、儲存配額耗盡、並行掃描和掃描器故障;
- 檔案在通過掃描後被替換的競態,以及重複完成請求造成的狀態錯亂。
20 MiB、三種允許類型和 B2B 多租戶是面試情境約束,不是通用安全門檻。題目要求候選人把身分、上傳協定、物件儲存、非同步工作和下載授權組成一個可證明的安全邊界,核心能力屬於後端 API 與服務安全,因此歸為 backend。
面試官考察重點
第一,看候選人是否先建立不變條件:未通過安全判定的位元組只能留在隔離區,不能被業務使用者讀取、解析或從應用程式主網域提供;用戶端檔名不能成為儲存路徑;每次讀取都要重新授權。
第二,看是否理解「驗證類型」需要縱深防禦。副檔名、請求 Content-Type、檔案簽名字節和解析器結果各自只能提供部分證據。業務允許清單、正規化、結構解析、惡意軟體掃描,以及適用時的重新編碼或內容淨化應組合使用。
第三,看能否設計清楚的非同步狀態機。上傳成功只代表位元組已進入隔離區,不能直接回傳「檔案可用」。API 需要揭露處理中、可用、拒絕和處理失敗等狀態,並明確定義重試、逾時和清理規則。
第四,看是否發現掃描競態。掃描器判定的是某一份確定位元組;若同一個物件鍵可以在掃描後被覆寫,AVAILABLE 狀態可能指向另一份從未掃描的內容。掃描結論必須綁定不可變物件版本或內容雜湊,發布時做條件更新。
第五,看是否把下載也納入威脅模型。隨機鍵只降低猜測機會,不能取代租戶與物件層級授權。下載應使用受控處理器或短效簽章位址,並設定安全的回應標頭、獨立來源和最小快取策略。
回答前需要釐清的問題
- 檔案會被怎樣使用? 僅下載、瀏覽器預覽、文字擷取、縮圖和第三方處理的風險不同;每增加一個解析器,就增加一個攻擊面。
- 三種類型是否足夠? 本題可直接拒絕 ZIP、Office 和其他格式;若允許壓縮檔,必須再限制層數、檔案數、解開後大小和壓縮比。
- 使用者如何驗證身分? Cookie 會引出 CSRF 防護;Bearer token 仍需正確的 CORS、權限範圍和洩漏控制。
- 是否需要直接上傳物件儲存? 小檔案可以由 API 串流接收;高並行時可發放短效、單一物件、僅寫入隔離區的上傳憑證,完成後由伺服器端核對實際物件。
- 誰能上傳、查看狀態、下載和刪除? 應分別定義租戶角色、物件歸屬和稽核要求,不能只檢查「已登入」。
- 掃描服務的 SLA 是什麼? 需要確定等待時間、重試次數、故障時是否 fail closed,以及使用者看到的狀態。
- 是否有法遵或資料駐留要求? 加密金鑰、保留期、稽核日誌、刪除和備份清理都可能受約束。
- 成功標準是什麼? 上傳位元組落盤、掃描通過和授權使用者可下載是三個不同里程碑,API 語意要分別表達。
30 秒回答框架
「我先定義三個不變條件:未通過掃描的檔案只在隔離區;儲存鍵由伺服器端產生,原始檔名僅作為受限中繼資料;任何下載都執行租戶和物件層級授權。流程使用 PENDING_UPLOAD → QUARANTINED → SCANNING → AVAILABLE/REJECTED/FAILED 狀態機。
建立上傳時驗證使用者權限、配額、允許類型和 20 MiB 上限,產生不可猜且不可覆寫的物件鍵。位元組串流進入無執行權限、無公開讀取權限的隔離儲存;伺服器端根據實際物件大小、正規化副檔名、檔案簽名和受限解析結果驗證,再由隔離的掃描 worker 做惡意軟體偵測。掃描結論綁定物件版本或 SHA-256,只有條件更新仍相符時才發布。
下載時重新驗證租戶和物件權限,再回傳短效簽章位址或由伺服器端串流輸出,並設定 Content-Disposition: attachment、準確的安全類型和 X-Content-Type-Options: nosniff。掃描失敗時保持不可用並限次重試;配額、速率、並行、逾時和生命週期清理控制資源。最後用類型欺騙、路徑檔名、測試病毒、超限串流、重複完成、物件替換和跨租戶存取做對抗性驗證。」
分步深入解答
第一步:先寫出信任邊界和安全不變條件
用戶端可以控制 multipart 部件的位元組、檔名、Content-Type、宣告大小和請求順序。API 閘道、應用程式服務、物件儲存事件和掃描佇列也可能重複、延遲或亂序。設計應圍繞以下不變條件:
- 隔離物件沒有公開讀取權限,也不會由 Web 伺服器執行;
- 只有
AVAILABLE且授權通過的物件可以下載; - 業務紀錄中的
tenant_id來自驗證身分的上下文,不接受用戶端自由指定; - 原始檔名不參與路徑串接或物件定位;
- 安全判定綁定一份不可變位元組,發布過程不能把結論移植到另一個版本;
- 任何異常、逾時或無法判定都保持 fail closed。
「檔名隨機化」和「物件不可公開」解決不同問題。隨機物件鍵防止覆寫、路徑操縱和容易猜測;授權與私有儲存負責機密性。兩者都要有。
第二步:用狀態機分開上傳成功與檔案可用
建議的最小狀態機如下:
PENDING_UPLOAD
├─ bytes verified ─> QUARANTINED ─> SCANNING
│ ├─ safe ─> AVAILABLE
│ ├─ malicious/invalid ─> REJECTED
│ └─ scanner unavailable ─> FAILED
└─ expired/abandoned ─> EXPIREDPOST /uploads 建立工作階段並回傳 upload_id。若採用直接上傳,它同時回傳只能寫入指定隔離物件、短時有效的憑證。POST /uploads/{id}/complete 只觸發伺服器端核驗與掃描,適合回傳 202 Accepted。GET /uploads/{id} 回傳狀態;只有 AVAILABLE 才提供下載入口。
每個遷移都使用資料庫條件更新,例如只有目前為 QUARANTINED 的紀錄才能進入 SCANNING。佇列重複投遞和用戶端重複呼叫 complete 時,同一個狀態遷移最多生效一次。FAILED 表示技術處理失敗,REJECTED 表示檔案或策略不合格,兩者不能混為一個模糊錯誤。
第三步:安全接收位元組並控制資源
建立工作階段時先檢查上傳權限、租戶剩餘配額、使用者速率和允許的業務類型。伺服器端產生隨機 upload_id 與物件鍵,例如 quarantine/{tenant-id}/{uuid};原始檔名經過長度、控制字元和 Unicode 正規化處理後,只作為顯示中繼資料保存。
若 API 代理上傳,應串流寫入隔離儲存並邊讀邊計數,超過 20 MiB 立即中止,不能把整個檔案讀進程序記憶體。若用戶端直接上傳,簽章能力應盡量限制方法、物件鍵、有效期和大小;直接上傳完成後仍要由可信任服務讀取物件中繼資料並核對實際長度、版本與所有權,不能相信用戶端的 complete 請求。
隔離 bucket 或目錄預設私有、禁止執行、與應用程式主網域隔離,並使用最小權限帳號。上傳工作階段到期、用戶端放棄和掃描終態都要有生命週期清理,避免孤兒物件長期占用配額。
第四步:組合類型驗證、解析與惡意內容掃描
驗證順序可以先便宜後昂貴,但任何單層都不能直接產生 AVAILABLE:
- 對正規化後的副檔名執行 PDF、JPEG、PNG 允許清單;
- 把用戶端
Content-Type當作提示,拒絕明顯不一致,但不把它當證據; - 檢查檔案簽名和完整結構,防止只在開頭放合法 magic bytes;
- 使用保持更新且受限執行的解析器確認檔案可完整解析;
- 在隔離 worker 中執行惡意軟體掃描,記錄引擎與規則版本;
- 對圖片可解碼後重新編碼,對 PDF 可依業務風險使用內容淨化。
掃描器和解析器處理攻擊者位元組,應執行在受限 CPU、記憶體、暫存磁碟、時間和網路權限的沙箱中。題目不允許壓縮檔,直接拒絕可減少解壓縮炸彈、巢狀深度和路徑穿越風險。防毒結果也不是絕對安全證明,儲存隔離、安全下載和最小解析仍需保留。
第五步:把掃描結論綁定到不可變物件再發布
掃描工作讀取隔離物件的不可變 versionid,並計算 SHA-256。工作結果至少包含 uploadid、物件版本、雜湊、實際大小、偵測類型、掃描引擎版本和 verdict。
發布時資料庫執行條件遷移:紀錄仍為 SCANNING,物件版本與雜湊仍等於工作輸入,才可設為 AVAILABLE。物件儲存同時禁止覆寫該版本;若需要把檔案複製到 approved 區,複製目標也使用新隨機鍵,並核對複製來源版本。任何不相符都重新隔離或拒絕,不能沿用舊 verdict。
這項綁定關閉了典型的檢查與使用時間差:攻擊者不能先上傳安全檔案取得通過結果,再用同一鍵覆寫成惡意檔案。若儲存沒有版本能力,可使用寫入一次物件鍵或以內容雜湊定址,並讓業務紀錄只指向最終不可變物件。
第六步:授權下載並控制瀏覽器解釋方式
GET /files/{id}/download 先用驗證身分的上下文查找目前租戶,再檢查物件歸屬、角色、刪除狀態和 AVAILABLE 狀態。物件 ID 即使是 UUID,也不能免除這些檢查。通過後可由應用程式串流回傳,或產生綁定具體物件、操作和短有效期的簽章下載位址。
對不需要瀏覽器預覽的附件,回應使用 Content-Disposition: attachment、經過安全編碼的顯示檔名、伺服器端確認的媒體類型,以及 X-Content-Type-Options: nosniff。檔案來源與主應用程式 Cookie 網域隔離,可減少主動內容影響主站工作階段的機會。簽章位址的有效期要短,因為刪除權限後,已經發出的位址通常不會立即失效。
下載端還要限制速率與頻寬、稽核授權結果,並決定代理或快取是否允許保存私有回應。日誌記錄物件 ID、租戶、操作者和結果,避免記錄檔案正文或可長期存取的簽章 URL。
第七步:設計失敗、冪等、配額與可觀測性
掃描逾時或服務不可用時,檔案保持不可下載。worker 可依退避策略限次重試;超過次數進入 FAILED,由人工或受控重試恢復。掃描器回傳明確惡意或結構不合法時進入 REJECTED,並依保留策略刪除隔離位元組。
建立介面可接受冪等鍵,避免用戶端逾時重試產生多個工作階段;complete、掃描消費和刪除都以條件狀態遷移實現冪等。並行限制至少涵蓋使用者上傳數、租戶總位元組、單檔大小、掃描佇列長度和每租戶掃描槽位,防止一個租戶擠占全域資源。
關鍵指標包括各狀態停留時間、隔離區位元組數、過期工作階段數、掃描通過/拒絕/失敗率、重試次數、佇列年齡、解析器逾時、跨租戶拒絕和下載授權失敗。稽核日誌記錄狀態遷移、物件版本、雜湊、策略與掃描版本,才能回答「哪一份位元組由什麼規則判定」。
第八步:用對抗性矩陣驗證完整鏈路
驗證至少包含:
.jpg.php、大小寫混合、空位元組和超長 Unicode 檔名;- 偽造
Content-Type、合法開頭加惡意尾部、損壞 PDF、多格式檔案; - 安全環境中的 EICAR 測試檔案,以及會觸發解析器逾時或資源上限的樣本;
- 精確 20 MiB、超出 1 位元組、無長度宣告、慢速串流和大量並行上傳;
- 相同冪等鍵、並行 complete、重複佇列訊息和亂序掃描結果;
- 掃描期間嘗試覆寫物件,確認版本或雜湊不相符時無法發布;
- 另一個租戶讀取狀態、下載或刪除物件,確認全部拒絕;
- 掃描器停機、工作逾時、應用程式重啟和孤兒物件清理;
- 下載回應的 attachment、媒體類型、
nosniff、快取和簽章過期行為。
通過條件應同時涵蓋安全與業務:被拒絕檔案永遠沒有下載入口,安全檔案最終可用,重試不產生重複紀錄,越權存取無資料洩漏,異常期間保持 fail closed,資源使用受限,並能從稽核紀錄還原完整判定鏈。
高品質示範回答
「我會把上傳設計為隔離、判定、發布三個階段,並先保證未通過判定的位元組永遠不可被業務使用者讀取。建立上傳工作階段時,從驗證身分的上下文確定 tenant_id,檢查角色、20 MiB 上限、租戶配額和 PDF/JPEG/PNG 允許清單,再產生隨機、不可覆寫的隔離物件鍵。原始檔名只保留為長度受限且正規化的顯示中繼資料。
位元組透過 API 串流寫入,或用短效且只允許寫指定隔離鍵的憑證直接上傳。complete 後,伺服器端核對實際大小、物件版本和歸屬,把紀錄從 PENDING_UPLOAD 條件更新到 QUARANTINED,再非同步掃描。驗證會組合副檔名、伺服器端偵測類型、完整結構解析、惡意軟體掃描和適用的重新編碼;掃描程序使用受限資源與網路權限。
掃描器讀取不可變版本並計算 SHA-256。只有紀錄仍處於 SCANNING,且版本和雜湊都與掃描輸入相同,才能條件更新為 AVAILABLE;這樣安全檔案通過後被覆寫也無法重用舊結論。掃描器故障保持 fail closed 並限次重試,明確惡意或不合規則進入 REJECTED。
下載介面每次檢查租戶、物件權限和 AVAILABLE 狀態,再串流輸出或簽發短效下載位址。附件使用獨立來源、Content-Disposition: attachment 和 X-Content-Type-Options: nosniff。最後我會驗證類型欺騙、EICAR、超限串流、重複 complete、物件覆寫競態、跨租戶讀取和掃描器停機,並監控狀態時間、隔離區位元組、佇列年齡和掃描版本。」
常見錯誤
- 只檢查副檔名 → 雙副檔名、大小寫和偽裝內容可以繞過 → 使用業務允許清單、正規化、簽名、結構解析與掃描的組合。
- 信任
Content-Type→ 用戶端可以任意填寫請求標頭 → 僅把它用於早期篩選,最終類型由伺服器端偵測。 - 使用原始檔名作為路徑 → 可能產生路徑穿越、覆寫和不同檔案系統正規化問題 → 伺服器端產生隨機物件鍵,原名只作受限中繼資料。
- 上傳落盤後立即可下載 → 掃描尚未完成,惡意位元組已經暴露 → 隔離儲存並以
AVAILABLE作為唯一發布條件。 - 防毒通過就宣稱絕對安全 → 新樣本、解析器漏洞和主動內容仍可能存在 → 保留隔離、最小解析、安全回應標頭與權限控制。
- 掃描同一個可覆寫鍵 → 通過結果可能被套用到後來替換的位元組 → 綁定不可變版本或內容雜湊,並條件發布。
- UUID 等於授權 → 知道或洩漏物件 ID 後可能跨租戶讀取 → 每次狀態查詢、下載和刪除都做物件層級授權。
- 直接上傳後相信用戶端 complete → 用戶端可偽造大小、類型或物件歸屬 → 伺服器端讀取物件中繼資料與位元組證據重新核對。
- 掃描器故障時自動放行 → 基礎設施故障變成安全繞過 → fail closed、限次重試並揭露清楚處理狀態。
- 只限制單一檔案大小 → 大量合法大小檔案仍可耗盡儲存與掃描能力 → 增加租戶配額、速率、並行和佇列背壓。
- 在主網域行內顯示任意檔案 → 瀏覽器解釋和主動內容可能影響主站工作階段 → 預設附件下載並隔離檔案來源。
追問及應對
追問一:有了 magic bytes,為什麼還要解析完整結構?
檔案簽名通常只涵蓋開頭少量位元組。攻擊者可以在合法標頭後放入另一種類型內容,或建構同時被多個解析器接受的檔案。完整結構解析能確認內部長度、物件關係和結束標記是否符合預期,但解析器本身也處理不可信任輸入,所以還要限制資源、及時更新並放在隔離環境。
追問二:直接上傳物件儲存會不會繞過後端安全?
直接上傳只改變位元組傳輸路徑。後端仍建立綁定租戶和物件鍵的短效上傳工作階段,憑證只能寫隔離區;完成後由後端核對實際物件、大小、版本和雜湊,再觸發掃描。物件在 AVAILABLE 前沒有讀取權限,因此用戶端無法透過直接上傳直接發布。
追問三:掃描器暫時不可用時,使用者體驗怎麼處理?
complete 回傳處理中狀態,查詢介面顯示仍在安全檢查或處理失敗可重試。系統限次退避重試,並監控佇列年齡;超過門檻進入 FAILED,仍保持不可下載。恢復後可由受控工作重試同一個不可變版本,不能為了可用性跳過判定。
追問四:為什麼下載還需要 attachment 和 nosniff?
授權解決誰能拿到位元組,回應標頭控制瀏覽器如何解釋位元組。attachment 傾向觸發下載,nosniff 阻止瀏覽器偏離伺服器端宣告類型進行嗅探。配合獨立檔案來源,可以縮小主動內容影響應用程式主域 Cookie 和指令碼上下文的範圍。
追問五:允許 ZIP 後要增加哪些控制?
需要在隔離沙箱中檢查每個成員路徑,拒絕絕對路徑和 ..,限制巢狀層數、成員數量、單一成員大小、總解開大小、壓縮比、CPU 和時間,並對解開後的每個檔案重新執行類型與惡意內容檢查。本題業務不需要 ZIP,直接拒絕是更小的攻擊面。
追問六:怎樣證明掃描結果沒有被競態污染?
讓掃描工作記錄輸入物件的不可變版本和 SHA-256;發布使用資料庫條件更新,同時驗證目前紀錄仍為 SCANNING 且版本、雜湊完全相同。測試在掃描期間覆寫同一邏輯物件並投遞舊結果,預期發布失敗。稽核日誌應能串起輸入版本、掃描 verdict 與最終 approved 物件。