不可信任 $_FILES['file']['type'](瀏覽器根據副檔名猜測,易偽造)。必須讀取檔案頭部「魔數」或使用 finfo 擴展檢測真實 MIME 類型。
| 格式 | 魔數 (十六進制) | ASCII 表示 |
|---|---|---|
| JPEG | FF D8 FF | ÿØÿ |
| PNG | 89 50 4E 47 0D 0A 1A 0A | ‰PNG··· |
| GIF87a/89a | 47 49 46 38 37/39 61 | GIF87a / GIF89a |
| 25 50 44 46 | ||
| ZIP / DOCX / XLSX / APK | 50 4B 03 04 | PK·· |
| RAR | 52 61 72 21 1A 07 | Rar!·· |
| BMP | 42 4D | BM |
| MP3 (ID3) | 49 44 33 | ID3 |
| WAV | 52 49 46 46 | RIFF |
| AVI | 52 49 46 46....41 56 49 | RIFF....AVI |
function get_real_mime($filepath) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
return $finfo->file($filepath);
}
// 使用
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$mime = get_real_mime($_FILES['file']['tmp_name']);
if (!in_array($mime, $allowed)) {
die('檔案類型不允許: ' . $mime);
}
--with-mime-magic 或安裝 fileinfo 擴展function get_mime_by_magic($filepath) {
$fp = fopen($filepath, 'rb');
$header = fread($fp, 8); // 讀前 8 字節足夠大多數格式
fclose($fp);
$hex = bin2hex($header);
$signatures = [
'ffd8ff' => 'image/jpeg',
'89504e470d0a1a0a' => 'image/png',
'474946383761' => 'image/gif',
'474946383961' => 'image/gif',
'25504446' => 'application/pdf',
'504b0304' => 'application/zip', // 含 docx/xlsx
'526172211a07' => 'application/x-rar-compressed',
'424d' => 'image/bmp',
'494433' => 'audio/mpeg',
'52494646' => 'audio/wav', // 需進一步判斷 WAVE/AVI
];
foreach ($signatures as $sig => $mime) {
if (strpos($hex, $sig) === 0) return $mime;
}
return 'application/octet-stream';
}
function get_mime_by_file_cmd($filepath) {
$out = shell_exec('file --brief --mime-type ' . escapeshellarg($filepath));
return trim($out);
}
優點:系統級魔數庫最全;缺點:依賴外部命令、效能較差、Windows 不通用。
function validate_upload($file, array $allowedMimes, int $maxSize = 10485760) {
// 1. PHP 錯誤碼
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['ok' => false, 'msg' => '上傳錯誤: ' . $file['error']];
}
// 2. 大小
if ($file['size'] > $maxSize) {
return ['ok' => false, 'msg' => '檔案過大'];
}
// 3. 真實 MIME
$finfo = new finfo(FILEINFO_MIME_TYPE);
$realMime = $finfo->file($file['tmp_name']);
if (!in_array($realMime, $allowedMimes)) {
return ['ok' => false, 'msg' => "類型不允許: $realMime"];
}
// 4. 副檔名一致性(可選)
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$extMimes = [
'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg',
'png' => 'image/png', 'gif' => 'image/gif',
'pdf' => 'application/pdf', 'zip' => 'application/zip',
];
if (isset($extMimes[$ext]) && $extMimes[$ext] !== $realMime) {
return ['ok' => false, 'msg' => '副檔名與內容不符'];
}
return ['ok' => true, 'mime' => $realMime];
}
function sanitize_image($src, $dest) {
$info = getimagesize($src);
if (!$info) return false;
$mime = $info['mime'];
$create = 'imagecreatefrom' . ($mime === 'image/jpeg' ? 'jpeg' : ($mime === 'image/png' ? 'png' : 'gif'));
if (!function_exists($create)) return false;
$img = $create($src);
if (!$img) return false;
$new = imagecreatetruecolor($info[0], $info[1]);
if ($mime === 'image/png' || $mime === 'image/gif') {
imagealphablending($new, false);
imagesavealpha($new, true);
$transparent = imagecolorallocatealpha($new, 0,0,0,127);
imagefill($new, 0, 0, $transparent);
}
imagecopyresampled($new, $img, 0,0, 0,0, $info[0], $info[1], $info[0], $info[1]);
$outFunc = 'image' . ($mime === 'image/jpeg' ? 'jpeg' : ($mime === 'image/png' ? 'png' : 'gif'));
$result = $outFunc($new, $dest, 90);
imagedestroy($img); imagedestroy($new);
return $result;
}
將上傳圖片重新繪製保存,可剝離 EXIF、隱寫代碼、惡意腳本片段。
fileinfo 擴展**,使用 finfo 為首選Content-Disposition: attachment/usr/share/misc/magic 魔數資料庫