檔案上傳驗證、下載回應標頭、前端接受屬性設定皆需正確的 MIME 類型。瀏覽器對同一副檔名回傳的 $_FILES['file']['type'] 可能不同(IE vs Firefox vs Chrome),導致白名單驗證失效。本文提供**完整對照表**、**瀏覽器差異分析**、**PHP 驗證最佳實踐**。
| 副檔名 | IE 11 | Firefox | Chrome/Edge | 標準 RFC |
|---|---|---|---|---|
| .jpg | image/jpeg | image/pjpeg | image/jpeg | image/jpeg |
| .png | image/png | image/x-png | image/png | image/png |
| .zip | application/zip | application/x-zip-compressed | application/zip | application/zip |
| application/pdf | application/pdf | application/pdf | application/pdf | |
| .js | application/x-javascript | text/html | application/javascript | application/javascript |
| .svg | image/svg+xml | image/svg+xml | image/svg+xml | image/svg+xml |
| .woff2 | application/octet-stream | font/woff2 | font/woff2 | font/woff2 |
結論:**不可信任 $_FILES['type']**,必須在伺服器端以副檔名或檔案簽名(Magic Bytes)判斷。
$mimeMap = [
// 圖片
'gif' => 'image/gif',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'bmp' => 'image/bmp',
'svg' => 'image/svg+xml',
'ico' => 'image/x-icon',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
// 文檔
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'txt' => 'text/plain',
'rtf' => 'application/rtf',
'csv' => 'text/csv',
// 壓縮/封裝
'zip' => 'application/zip',
'rar' => 'application/vnd.rar',
'7z' => 'application/x-7z-compressed',
'gz' => 'application/gzip',
'tar' => 'application/x-tar',
// 音視頻
'mp3' => 'audio/mpeg',
'wav' => 'audio/wav',
'mp4' => 'video/mp4',
'avi' => 'video/x-msvideo',
'mov' => 'video/quicktime',
'flv' => 'video/x-flv',
'mkv' => 'video/x-matroska',
'webm' => 'video/webm',
// 字體
'woff' => 'font/woff',
'woff2' => 'font/woff2',
'ttf' => 'font/ttf',
'otf' => 'font/otf',
'eot' => 'application/vnd.ms-fontobject',
// 程式/腳本
'js' => 'application/javascript',
'json' => 'application/json',
'xml' => 'application/xml',
'html' => 'text/html',
'htm' => 'text/html',
'css' => 'text/css',
'php' => 'application/x-httpd-php', // 僅供參考,上傳 PHP 極度危險
// 其他
'apk' => 'application/vnd.android.package-archive',
'ipa' => 'application/vnd.iphone',
'exe' => 'application/octet-stream',
'dll' => 'application/octet-stream',
'bin' => 'application/octet-stream',
];
function validateUpload(array $file, array $allowedExt, array $allowedMime, int $maxSize = 5 * 1024 * 1024)
{
// 1. 基礎錯誤碼檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('上傳錯誤代碼:' . $file['error']);
}
// 2. 檔案大小
if ($file['size'] > $maxSize) {
throw new Exception('檔案超過大小限制');
}
// 3. 副檔名白名單(正規化小寫)
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt, true)) {
throw new Exception("副檔名 '$ext' 不在允許清單中");
}
// 4. MIME 類型檢查(finfo 讀取檔案簽名,不信任瀏覽器提供的 type)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$realMime = $finfo->file($file['tmp_name']);
if (!in_array($realMime, $allowedMime, true)) {
throw new Exception("檔案實際類型 '$realMime' 不被允許");
}
// 5. 圖片額外驗證(getimagesize 確認是真圖片、取得寬高)
if (strpos($realMime, 'image/') === 0) {
$imgInfo = getimagesize($file['tmp_name']);
if ($imgInfo === false) throw new Exception('非有效圖片檔案');
// 可進一步限制尺寸:$imgInfo[0] > 5000 等
}
return ['ext' => $ext, 'mime' => $realMime];
}
// 使用範例
try {
$info = validateUpload($_FILES['avatar'], ['jpg','png','webp'], ['image/jpeg','image/png','image/webp']);
// 移動檔案、寫入資料庫...
} catch (Exception $e) {
// 記錄日誌、回傳錯誤訊息
}
header('Content-Type: ' . $mimeMap[$ext]);
header('Content-Disposition: attachment; filename="' . rawurlencode($originalName) . '"');
header('Content-Length: ' . filesize($path));
readfile($path); // 高效輸出
<input accept> 設定$mimeMap 獨立為 config/mime.php,併入版控,新增類型只改設定檔。$_FILES['type'] 或 $_FILES['name']。php,php5,phtml,php7,asp,aspx,jsp,exe,bat,sh)列入**永久黑名單**,無論 MIME 為何直接拒絕。建立標準化 MIME 管理機制,可徹底解決「瀏覽器上傳類型不一致導致誤判」、「惡意檔案偽裝上傳」等經典安全隱患。