題幹與適用場景
一個壓縮檔解壓服務會把使用者上傳的檔案寫入 /srv/uploads/job-42。壓縮檔項目的檔名完全由外部輸入提供,攻擊者可能提交 ../../etc/passwd、絕對路徑,或指向目錄外的符號連結。服務需要建立目錄、寫入檔案,並保證每次檔案操作都不能越過工作目錄。
請用 Go 1.24 或更新版本說明實作。假設程序使用一般應用程式權限,Linux 是主要生產平台,但客戶端也可能在 Windows 或 WASI 執行。回答要區分「檔名不受信任」和「使用者明確選擇任意輸出目錄」兩種需求。
面試官考察點
- 是否先定義根目錄與攻擊者可控輸入,再選擇目錄限制 API,而不是只做字串替換。
- 是否知道
os.Root會阻止..與越過根目錄的符號連結,並理解它和filepath.Clean的邊界不同。 - 是否能辨識
EvalSymlinks再檢查存在 TOCTOU 視窗,說明為何要讓一次開啟操作承擔限制。 - 是否處理
Root、檔案控制代碼與暫存檔的關閉、失敗清理和權限策略。 - 是否主動說明 bind mount、
GOOS=js、WASI 實作能力及過深目錄帶來的限制。
回答前需要澄清的問題
- 要解壓讀取、寫入,還是兩者都要?唯讀和建立需要不同的
OpenFile旗標與權限。 - 根目錄是服務固定的,還是呼叫者可選?若呼叫者可以選任意目錄,
os.Root不會額外形成沙箱邊界。 - 是否允許壓縮檔建立符號連結、硬連結、裝置檔或重新命名?不允許的類型要在壓縮檔解析層拒絕,不能期待
Root補齊所有策略。 - 目標平台是否包含
GOOS=js或 WASI?官方資料指出這些平台的路徑安全保證和 Unix 檔案描述元實作不同。
30 秒回答框架
「我會把工作目錄當成唯一根,所有外部項目都作為相對名稱交給 os.OpenRoot 回傳的 Root,不再用 filepath.Join 後呼叫普通 os.Create。建立、開啟和刪除都透過 Root 方法執行;.. 或逃逸符號連結直接失敗。寫入採用暫存檔與受限權限,再按業務規則提交。測試涵蓋路徑逃逸、符號連結競態、並行寫入、Windows 保留裝置名稱和根目錄關閉。Root 能防止目錄外路徑,但不能取代壓縮檔類型檢查、權限隔離或 bind mount 防護。」
分步驟深入解答
1. 先固定安全邊界
不要把「不包含 ..」當成安全定義。攻擊者可以利用符號連結,或在檢查和開啟之間替換目錄項目。安全不變量應寫成:每次由壓縮檔名稱觸發的檔案系統操作,其解析結果都位於工作根目錄樹內;任何無法證明的操作都失敗。
Go 1.24 的 os.OpenRoot 會開啟根目錄,Root.Open、Root.Create、Root.OpenFile、Root.Mkdir 和 Root.Stat 等方法接受相對於該根的名稱。官方實作會拒絕透過 .. 或符號連結離開根目錄。根物件應在任務生命週期內重複使用,並在所有檔案控制代碼關閉後 Close。
func writeEntry(rootDir, name string, data []byte) error {
root, err := os.OpenRoot(rootDir)
if err != nil {
return err
}
defer root.Close()
f, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
return err
}這個範例保證名稱解析不會逃逸,但生產實作仍要限制檔案大小、項目數量和目錄深度,並在寫入失敗時刪除不完整檔案。O_EXCL 只能表達「不覆寫已存在檔案」;它不負責壓縮檔去重或業務冪等。
2. 解釋為何字串清理不足
filepath.Clean 可以把 a/../b 正規化,卻不能證明 a 不是指向根目錄外的符號連結。先呼叫 EvalSymlinks、再檢查前綴也有 TOCTOU:檢查完成後,目錄項目可能被替換。普通 filepath.Join 加 os.Create 更沒有目錄邊界保證。
Root 在 Unix 上以根目錄檔案描述元與相對開啟操作為基礎,限制解析過程,而不是把安全性寄託在一次獨立檢查。它允許根內相對路徑和根內符號連結;../ 指向根外或絕對符號連結則應失敗。呼叫者仍需拒絕壓縮檔中的符號連結項目,除非產品明確支援它們。
3. 處理壓縮檔類型與寫入流程
解壓器先讀取項目類型、大小、權限和名稱,拒絕裝置檔、FIFO、硬連結以及產品不允許的符號連結。對普通檔案,使用 Root.Mkdir 或 MkdirAll 建立父目錄,再用 Root.OpenFile 建立目標;對目錄,使用 Root.Mkdir。總大小、單檔大小、路徑元件數和並行任務數都要有預算。
更穩妥的寫入流程是「暫存名稱 → 完整寫入 → 校驗 → 原子提交」。暫存名稱也必須透過 Root 建立,避免暫存檔落在系統暫存目錄後再跨目錄移動。若 Root 版本缺少所需的重新命名能力,應把這項能力作為平台相容層或產品限制明確化,不能偷偷退回不受限制的普通路徑 API。
4. 說明平台與權限邊界
官方資料指出,Unix 實作通常追蹤根目錄檔案描述元,根目錄重新命名後仍指向原目錄;Windows 使用控制代碼並阻止某些保留裝置名稱。GOOS=js 的檔案 API 缺少 openat 家族呼叫,符號連結驗證存在 TOCTOU 限制;WASI 的保證取決於具體實作。Root 也不阻止 Linux bind mount、/proc 特殊檔案或 Unix 裝置檔存取。
因此,容器隔離、掛載策略、程序權限和壓縮檔類型白名單仍是獨立控制。應用程式不應把 Root 描述成完整容器沙箱,也不應把客戶端選擇的任意目錄誤稱為受限根目錄。
5. 設計可執行的驗證
測試至少包含:../escape、絕對路徑、根內符號連結、指向根外的符號連結、名稱以 ../ 結尾、並行建立同一檔案、超大項目和根目錄關閉後的呼叫。Go 1.24.3 修復了 Root 路徑以 ../ 結尾時可能開啟父目錄的問題,因此 CI 應固定包含修復版本的工具鏈,並保留這個回歸案例。
在 Linux 上用暫存目錄和真實符號連結測試成功與失敗路徑;用 -race 檢查共享狀態,但不能把 race detector 當作檔案系統邊界證明。跨平台建置不能證明每個平台擁有相同的核心保證,必須把 GOOS 作為測試矩陣的一部分。
高品質示範回答
「我先把工作目錄固定為根,規定不受信任的壓縮檔名稱只能作為 os.Root 的相對路徑。解壓流程拒絕裝置檔、硬連結和不允許的符號連結,限制項目數量、單檔大小和目錄深度;普通檔案透過 Root.OpenFile 建立,父目錄也透過 Root 建立,寫入失敗就刪除暫存目標。這樣 .. 和指向根外的符號連結由同一次受限制的開啟操作拒絕,避免先清理、再開啟的 TOCTOU 視窗。
我不會把它說成完整沙箱:Linux bind mount、程序權限、容器掛載和壓縮檔類型策略仍要分開處理。主要生產平台用真實符號連結和並行測試驗證逃逸、重複建立、資源關閉和路徑末尾 ../;Windows、WASI 與 GOOS=js 分別驗證它們的實作邊界。若產品要求呼叫者寫入任意目錄,我會取消 Root 的假設,改用明確的權限與稽核策略。」
常見錯誤
- 錯誤表現:
filepath.Join(base, name)後直接os.Create→ 失敗原因:..和符號連結可以讓解析結果離開 base → 修正方法: 固定根並透過Root方法執行每次操作。 - 錯誤表現: 先
EvalSymlinks檢查,再呼叫普通開啟 → 失敗原因: 檢查與使用之間存在 TOCTOU → 修正方法: 讓目錄限制參與實際開啟,並用競態測試驗證失敗路徑。 - 錯誤表現: 認為
os.Root會阻止所有檔案系統越界 → 失敗原因: bind mount、裝置檔和權限不屬於同一 API 保證 → 修正方法: 加上掛載、權限和壓縮檔類型白名單。 - 錯誤表現: 忽略
GOOS=js、WASI 和 Go 修補版本 → 失敗原因: 平台實作和安全修復並不一致 → 修正方法: 固定工具鏈,建立跨平台矩陣和針對性回歸案例。 - 錯誤表現: 只測試成功解壓 → 失敗原因: 路徑安全的證據來自拒絕逃逸與資源清理 → 修正方法: 測試
..、絕對路徑、符號連結、並行衝突、超限和關閉後的呼叫。
追問及應對
追問一:如果壓縮檔包含指向根內的符號連結,應該允許嗎?
先根據產品需求決定。若必須保留連結,應透過 Root 驗證連結目標仍在根內,並限制連結數量與深度;若只是解壓普通檔案,拒絕符號連結更容易稽核。無論選擇哪種策略,都不能讓 Root 自動取代壓縮檔類型白名單。
追問二:Linux bind mount 如何處理?
Root 不提供 bind mount 隔離。生產環境應讓容器或宿主機掛載策略把工作目錄放在專用檔案系統,限制應用程式權限,並在部署檢查中禁止不受信任的掛載。若威脅模型包含特權攻擊者,需要更強的沙箱或獨立 worker,而不是繼續堆疊路徑字串檢查。
追問三:為什麼不能把暫存檔寫到系統暫存目錄,再移到根目錄?
跨目錄移動會引入權限、掛載和原子性差異,而且暫存檔本身可能外洩。暫存名稱應透過 Root 建立並在同一根內完成寫入和提交;若平台 API 無法滿足原子提交,就明確降低功能或採用經過稽核的相容實作,不能無提示地退回普通路徑拼接。
追問四:GOOS=js 是否可以宣稱同樣安全?
不能。官方說明該平台缺少 openat 家族能力,符號連結驗證存在 TOCTOU 風險。應在建置目標上收緊功能、使用瀏覽器或執行環境提供的沙箱,或把不受信任檔案處理移到具備更強目錄限制的伺服器端;文件和 API 必須公開這項差異。