核心驗證函數庫
1. 副檔名白名單驗證
/**
* 檢查上傳檔案副檔名是否在允許清單中
* @param string $filename 檔案名稱
* @param array $allowedExt 允許的副檔名陣列,如 ['jpg','png','gif']
* @return bool|string 通過回傳 true,失敗回傳錯誤訊息
*/
function validateExtension(string $filename, array $allowedExt) {
if (strlen(trim($filename)) < 5) return '未選擇檔案';
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (empty($ext)) return '無法識別副檔名';
$allowed = array_map('strtolower', $allowedExt);
if (!in_array($ext, $allowed)) {
return '禁止的檔案類型:.' . $ext . ',允許類型:.' . implode(', .', $allowed);
}
return true;
}
2. 圖片尺寸驗證
/**
* 檢查圖片寬高是否超過限制
* @param string $tmpPath 暫存檔路徑 ($_FILES['file']['tmp_name'])
* @param int $maxWidth 最大寬度
* @param int $maxHeight 最大高度
* @return bool|string
*/
function validateImageSize(string $tmpPath, int $maxWidth, int $maxHeight) {
$info = @getimagesize($tmpPath);
if (!$info) return '非有效圖片檔案或檔案損壞';
[$width, $height, $type, $attr] = $info;
if ($width > $maxWidth || $height > $maxHeight) {
return "圖片尺寸 {$width}x{$height} 超過限制 {$maxWidth}x{$maxHeight}";
}
return true;
}
3. MIME 類型二次驗證(防偽裝)
/**
* 結合 finfo 進行真實 MIME 偵測
* @param string $tmpPath
* @param array $allowedMime 如 ['image/jpeg','image/png','image/gif']
* @return bool|string
*/
function validateMimeType(string $tmpPath, array $allowedMime) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpPath);
if (!in_array($mime, $allowedMime)) {
return "檔案真實類型 {$mime} 不在允許清單中";
}
return true;
}
整合上傳處理流程
<?php
// config
$config = [
'maxSize' => 2 * 1024 * 1024, // 2MB
'allowedExt' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
'allowedMime' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'maxWidth' => 1920,
'maxHeight' => 1080,
'uploadDir' => __DIR__ . '/uploads/',
'prefix' => 'img_',
];
// 確保目錄存在
if (!is_dir($config['uploadDir'])) mkdir($config['uploadDir'], 0755, true);
$errors = [];
$file = $_FILES['image'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
$errors[] = '上傳失敗,錯誤代碼:' . ($file['error'] ?? '未知');
} else {
// 1. 檔案大小
if ($file['size'] > $config['maxSize']) {
$errors[] = '檔案大小超過 ' . ($config['maxSize']/1024/1024) . 'MB 限制';
}
// 2. 副檔名
$res = validateExtension($file['name'], $config['allowedExt']);
if ($res !== true) $errors[] = $res;
// 3. MIME 類型
$res = validateMimeType($file['tmp_name'], $config['allowedMime']);
if ($res !== true) $errors[] = $res;
// 4. 圖片尺寸
$res = validateImageSize($file['tmp_name'], $config['maxWidth'], $config['maxHeight']);
if ($res !== true) $errors[] = $res;
// 5. 通過所有驗證,移動檔案
if (empty($errors)) {
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$newName = $config['prefix'] . date('YmdHis') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$dest = $config['uploadDir'] . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
echo json_encode(['success' => true, 'filename' => $newName, 'path' => $dest]);
} else {
echo json_encode(['success' => false, 'message' => '檔案儲存失敗,請檢查目錄權限']);
}
exit;
}
}
// 輸出錯誤
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(';', $errors)]);
前端配合(AJAX 上傳)
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="image" id="image" accept="image/*" required />
<button type="submit">上傳</button>
<div id="preview"></div>
</form>
<script>
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const res = await fetch('upload.php', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
document.getElementById('preview').innerHTML =
`<img src="/uploads/${data.filename}" style="max-width:300px" /><p>上傳成功</p>`;
} else {
alert('失敗:' + data.message);
}
});
</script>
進階安全加固清單
| 風險 | 防禦措施 |
| 雙重副檔名 (shell.php.jpg) | 只取最後一個點後字串;重新命名存檔 |
| MIME 偽裝 | 使用 finfo 偵測真實類型 |
| 圖片內嵌惡意代碼 | GD/Imagick 重新繪製壓縮(imagecreatefromstring -> imagejpeg) |
| 目錄遍歷 (../../etc/passwd) | basename() 取純檔名;路徑拼接用實際路徑 |
| 大檔案 DoS | php.ini upload_max_filesize、post_max_size、Nginx client_max_body_size |
| 執行權限 | 上傳目錄設定 php_flag engine off / Nginx location ~ \.php$ { deny all; } |
| 檔案名碰撞 | 時間戳 + 隨機字串 + hash 命名 |
常見錯誤代碼對照表
UPLOAD_ERR_OK = 0 // 成功
UPLOAD_ERR_INI_SIZE = 1 // 超過 php.ini upload_max_filesize
UPLOAD_ERR_FORM_SIZE = 2 // 超過表單 MAX_FILE_SIZE
UPLOAD_ERR_PARTIAL = 3 // 部分上傳
UPLOAD_ERR_NO_FILE = 4 // 未選檔案
UPLOAD_ERR_NO_TMP_DIR = 6 // 缺少暫存目錄
UPLOAD_ERR_CANT_WRITE = 7 // 寫入失敗
UPLOAD_ERR_EXTENSION = 8 // PHP 擴充套件中斷