本文 AI 產出,尚未審核
ExpressJS (TypeScript) – 檔案上傳:檔案型別與安全性考量
簡介
在 Web 應用程式中,檔案上傳是最常見的功能之一,無論是使用者上傳大頭照、簡報檔,還是後台管理員匯入批次資料,都離不開 Express 與 TypeScript 的協作。
然而,檔案上傳同時也是攻擊者的入口點:不受控的檔案類型、過大的檔案、或是惡意的檔名,都可能導致 檔案執行、路徑穿越、磁碟資源耗盡 等安全問題。
本文將從 檔案型別驗證、儲存策略、常見陷阱 四個面向,說明在 Express + TypeScript 環境下,如何安全且有效地處理檔案上傳。
核心概念
1️⃣ 為什麼要限制檔案類型?
- 降低執行危險:上傳可執行檔(
.exe、.js)若被放置於可直接存取的路徑,可能被直接執行。 - 避免資訊洩漏:某些檔案(如
.env、.config)內含敏感設定,若被外部下載會造成重大風險。 - 提升使用者體驗:前端可即時提示不支援的檔案類型,減少無效的上傳請求。
2️⃣ 檔案型別驗證的兩個層面
| 層面 | 說明 | 常用方法 |
|---|---|---|
| 副檔名檢查 | 只看檔名的文字部分(example.jpg) |
path.extname() |
| MIME Type 檢查 | 依據檔案內容的 magic number 判斷真實類型 | file.mimetype(由 Multer 提供)或 file-type 套件 |
最佳實踐:同時檢查副檔名與 MIME Type,僅依賴副檔名容易被偽造。
3️⃣ Multer 與 TypeScript 的基本設定
// src/middleware/upload.ts
import multer, { FileFilterCallback } from 'multer';
import { Request } from 'express';
import path from 'path';
import { randomBytes } from 'crypto';
// 1. 設定暫存資料夾(建議放在專屬目錄,不在 public 之下)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.resolve(__dirname, '../../uploads/tmp'));
},
// 2. 產生唯一檔名,避免覆寫與路徑穿越
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
const name = randomBytes(16).toString('hex');
cb(null, `${name}${ext}`);
},
});
// 3. 檔案類型白名單(以 MIME 為主)
const allowedMimes = new Set([
'image/jpeg',
'image/png',
'application/pdf',
'text/plain',
]);
// 4. 自訂 fileFilter
function fileFilter(
req: Request,
file: Express.Multer.File,
cb: FileFilterCallback,
) {
// 先檢查 MIME
if (!allowedMimes.has(file.mimetype)) {
return cb(new Error('不支援的檔案類型'), false);
}
// 再檢查副檔名是否符合預期
const ext = path.extname(file.originalname).toLowerCase();
if (!['.jpg', '.jpeg', '.png', '.pdf', '.txt'].includes(ext)) {
return cb(new Error('副檔名不符合允許清單'), false);
}
cb(null, true);
}
// 5. 建立 Multer 實例,限制檔案大小(例:5 MB)
export const upload = multer({
storage,
fileFilter,
limits: { fileSize: 5 * 1024 * 1024 },
});
4️⃣ 進階:使用 file-type 讀取檔案內容做二次驗證
// src/utils/validateFile.ts
import { fromFile } from 'file-type';
import fs from 'fs';
/**
* 以檔案內容的 magic number 判斷真實 MIME
* @param filePath 暫存檔案完整路徑
* @param whitelist 允許的 MIME 列表
*/
export async function verifyFileContent(
filePath: string,
whitelist: Set<string>,
): Promise<boolean> {
const type = await fromFile(filePath);
if (!type) return false; // 無法判斷類型
return whitelist.has(type.mime);
}
在路由中結合上述工具:
// src/routes/file.ts
import express from 'express';
import { upload } from '../middleware/upload';
import { verifyFileContent } from '../utils/validateFile';
import path from 'path';
import fs from 'fs';
const router = express.Router();
const allowedMimes = new Set(['image/jpeg', 'image/png', 'application/pdf']);
router.post(
'/upload',
upload.single('file'), // 前端欄位名稱必須是 file
async (req, res) => {
if (!req.file) return res.status(400).json({ error: '未收到檔案' });
const tmpPath = req.file.path;
const isValid = await verifyFileContent(tmpPath, allowedMimes);
if (!isValid) {
// 刪除不合法檔案
fs.unlinkSync(tmpPath);
return res.status(400).json({ error: '檔案內容與類型不符' });
}
// 移動到正式儲存目錄(可依需求分類)
const finalDir = path.resolve(__dirname, '../../uploads/images');
const finalPath = path.join(finalDir, req.file.filename);
fs.renameSync(tmpPath, finalPath);
res.json({ message: '上傳成功', filename: req.file.filename });
},
);
export default router;
5️⃣ 防止路徑穿越與檔名注入
- 使用
path.resolve與path.join:保證最終路徑一定在預期目錄下。 - 不要直接使用使用者提供的檔名:即使使用
path.basename,仍可能因 Unicode 同形字或控制字元造成問題。 - 產生隨機檔名(如上例
randomBytes)是最安全的做法。
常見陷阱與最佳實踐
| 陷阱 | 為何危險 | 推薦解法 |
|---|---|---|
| 只檢查副檔名 | 攻擊者可改名為 .jpg 但內容是執行檔 |
同時檢查 MIME 與 magic number |
| 未限制檔案大小 | 大檔案會耗盡磁碟或記憶體 | 使用 limits.fileSize,並在前端提前檢查 |
將上傳目錄放在 public |
直接可透過 URL 下載,若檔案可執行會被執行 | 儲存於 非公開目錄,透過 API 控制下載權限 |
| 使用原始檔名作為儲存名稱 | 可能導致檔名衝突、路徑穿越或 XSS | 產生 UUID / randomHex,或使用 multer 的 filename 方式 |
| 未處理錯誤回傳 | 前端無法得知失敗原因,使用者體驗差 | 在 fileFilter、上傳失敗、驗證失敗時都回傳清晰的錯誤訊息 |
| 缺乏病毒/惡意程式掃描 | 上傳的檔案可能包含木馬 | 結合 ClamAV、Microsoft Defender API 或第三方掃描服務 |
最佳實踐清單
- 白名單:僅允許業務需要的 MIME/副檔名。
- 雙重驗證:MIME + magic number。
- 隨機檔名 + 分類目錄:避免衝突與資訊洩漏。
- 儲存於非公開目錄,使用授權的下載 API。
- 設定檔案大小與數量限制,保護資源。
- 使用 HTTPS,防止檔案在傳輸途中被竊取或篡改。
- 定期清理過期檔案,避免磁碟被填滿。
- 日誌紀錄:上傳成功/失敗、使用者 ID、IP,方便事後追蹤。
實際應用場景
📸 使用者大頭照上傳
- 只接受
image/jpeg、image/png,最大 2 MB。 - 上傳後即時產生 縮圖(Sharp)並存入
uploads/avatars/{userId}。 - 下載時透過
/users/:id/avatarAPI,檢查使用者是否有權限讀取。
📄 文件中心(PDF、Word)
- 白名單
application/pdf、application/vnd.openxmlformats-officedocument.wordprocessingml.document。 - 使用
file-type再次驗證,並在上傳完成後呼叫 ClamAV 進行病毒掃描。 - 檔案儲存於
uploads/documents/{projectId},只有加入該專案的成員可透過授權 API 下載。
🛠 後台批次匯入(CSV)
- 只允許
text/csv,大小上限 10 MB。 - 上傳後立即使用 fast-csv 解析,若發現不符合欄位規則則直接回滾並刪除檔案。
- 為防止惡意腳本注入,所有欄位皆做 資料類型驗證(數字、日期、枚舉)。
總結
檔案上傳是前後端互動的關鍵功能,但若缺乏嚴謹的 型別驗證 與 安全防護,極易成為系統漏洞的入口。透過本文的 Multer + TypeScript 範例,我們示範了:
- 雙重型別驗證(副檔名 + MIME + magic number)
- 安全儲存策略(隨機檔名、非公開目錄、大小限制)
- 常見陷阱 的避免方式與 最佳實踐 清單
在實務開發中,請根據業務需求調整白名單與儲存結構,並結合病毒掃描、日誌追蹤等額外措施,才能打造既 友善 又 安全 的檔案上傳體驗。祝開發順利,讓你的 Express + TypeScript 應用在檔案處理上更上一層樓!