檔案上傳系統設計 (3) —— 安全性防禦與驗證
如果你的網站允許使用者上傳任何東西,且沒有做足夠的驗證,那麼你的伺服器就相當於對全世界開放了「寫入權限」。
一位合格的後端工程師,不僅要讓功能跑通,更要防止駭客利用檔案上傳漏洞(File Upload Vulnerability)把你的伺服器變成他的礦機或跳板。這一篇,我們來疊疊層防禦。
一、 不要只相信副檔名:理解 Magic Number
駭客最常用的手法就是:寫一個 cmd.php(內容是執行惡意指令),然後把它改名為 cute_cat.jpg 上傳。
如果你的後端只檢查 .jpg 尾綴,你就中招了。一旦這個檔案在伺服器上被執行,你的系統權限就可能被奪走。
解決方案:檢查檔案識別碼 (Magic Numbers)
大多數檔案格式在二進制數據的最開頭幾個 Bytes,都有特定的標記。
- JPEG: 總是
FF D8 FF開頭。 - PNG: 總是
89 50 4E 47開頭。 - PDF: 總是
25 50 44 46開頭。
Node.js 實作範例
我們可以使用 file-type 套件,或者手動讀取 Buffer 的前幾個位元組:
const fs = require("fs");
function checkMagicNumber(filePath) {
const buffer = Buffer.alloc(4); // 讀取前 4 個位元組
const fd = fs.openSync(filePath, "r");
fs.readSync(fd, buffer, 0, 4, 0);
fs.closeSync(fd);
const hex = buffer.toString("hex").toUpperCase();
if (hex === "89504E47") return "image/png";
if (hex.startsWith("FFD8FF")) return "image/jpeg";
if (hex === "25504446") return "application/pdf";
return "unknown";
}二、 限制防護:防止資源耗盡 (DoS)
如果我不檢查檔案大小,駭客可以直接傳送一個 100GB 的「垃圾檔案」給你的 Server。
- 後場後果:你的硬碟空間會瞬間爆滿,導致資料庫和其他服務因為無法寫入 Log 而報廢。
防禦建議:
- 設定 Payload Limit:在 Web 框架(如 Express, Koa)或 Nginx 層級限制請求的最大 Body 大小。
- Multer 限制:javascript
const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 }, // 限制 5MB });
三、 目錄安全:移除「執行權限」
假設駭客費盡心思把一個惡意腳本傳到了你的 /uploads 資料夾,只要他 不能執行 它,那這個腳本就只是一堆廢物數據。
在 Linux 系統中,你應該確保上傳目錄不具備執行腳本的能力。
指令操作:
# 讓 uploads 資料夾下的檔案只有讀取權限,沒有執行權限
chmod -R 644 /var/www/my-app/uploads更進階的做法是,將儲存檔案的伺服器與運行代碼的伺服器 物理隔離(例如使用 S3 雲端儲存)。
四、 靜態資源分離:Nginx 的角色
新手常會直接用 Express 寫 app.use('/uploads', express.static('uploads')) 來展示圖片。
這在生產環境並非最佳實踐。
- 效能問題:Node.js 是單執行緒,用它來處理大量檔案的 IO 會拖慢你的商務邏輯查詢。
- 安全性:Nginx 作為專門處理靜態資源的伺服器,經過高度優化且比應用軟體層更難被攻破。
TIP
推薦架構: 前端 -> Nginx (攔截 /uploads 請求直接由硬碟讀取) -> Node.js (只處理 API 邏輯)。
總結
安全性沒有銀彈,只有不斷堆疊的防禦:
- Magic Number:檢查檔案的「真身」,不信副檔名。
- 大小限制:保護硬碟與頻寬。
- 路徑安全性:不使用原始原始檔名(這在上篇已強調過)。
- 許可權控管:禁止上傳目錄執行任何腳本。
下一篇,我們將聊聊「現代化」的作法:效能優化與大檔案處理,包含縮圖自動化與斷點續傳。
️ 進階挑戰
如果你正在做一個允許上傳頭像的功能,除了檢查檔案類型外,是否應該強制將上傳的圖片透過程式碼(如 Sharp)重新處理一次?這對安全性有什麼幫助?
延伸閱讀與資源
- OWASP: File Upload Security Guide
- Node.js: Security Best Practices