enctype="multipart/form-data" 表單upload_tmp_dir (預設系統暫存目錄)$_FILES 資訊(型別、大小、錯誤碼)move_uploaded_file() 移至永久儲存目錄<form action="upload.php" method="post" enctype="multipart/form-data">
<!-- 隱藏域限制最大檔案大小 (bytes),僅供瀏覽器參考,後端必須再驗證 -->
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2MB -->
<input type="file" name="avatar" accept="image/*">
<button type="submit">上傳</button>
</form>
| 鍵名 | 說明 | 範例 |
|---|---|---|
name | 用戶端原始檔名 | photo.jpg |
type | 瀏覽器回報 MIME (不可信) | image/jpeg |
tmp_name | 伺服器暫存路徑 | /tmp/phpA1B2C3 |
size | 檔案大小 (bytes) | 102400 |
error | 錯誤代碼 (0=成功) | 0 |
<?php
// upload.php
const MAX_SIZE = 2 * 1024 * 1024; // 2MB
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const ALLOWED_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const UPLOAD_DIR = __DIR__ . '/uploads/avatars/';
$field = 'avatar';
$response = ['success' => false, 'message' => '', 'path' => ''];
// 1. 檢查是否有檔案上傳
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK) {
$errors = [
UPLOAD_ERR_INI_SIZE => '超過 php.ini upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => '超過表單 MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => '檔案上傳不完整',
UPLOAD_ERR_NO_FILE => '未選擇檔案',
UPLOAD_ERR_NO_TMP_DIR => '找不到暫存目錄',
UPLOAD_ERR_CANT_WRITE => '寫入暫存失敗',
];
$response['message'] = $errors[$_FILES[$field]['error'] ?? '未知錯誤'];
json_exit($response);
}
// 2. 基本屬性驗證
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES[$field]['tmp_name']);
$ext = strtolower(pathinfo($_FILES[$field]['name'], PATHINFO_EXTENSION));
if (!in_array($mime, ALLOWED_MIME, true)) {
$response['message'] = '檔案類型不允許 (MIME)';
json_exit($response);
}
if (!in_array($ext, ALLOWED_EXT, true)) {
$response['message'] = '副檔名不允許';
json_exit($response);
}
if ($_FILES[$field]['size'] > MAX_SIZE) {
$response['message'] = '檔案過大';
json_exit($response);
}
// 3. 圖片二次驗證 (防偽裝腳本)
$imgInfo = getimagesize($_FILES[$field]['tmp_name']);
if (!$imgInfo || !in_array($imgInfo[2], [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF, IMAGETYPE_WEBP])) {
$response['message'] = '非有效圖片檔案';
json_exit($response);
}
// 4. 生成安全檔名 (避免覆蓋、路徑遍歷)
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = UPLOAD_DIR . $safeName;
if (!is_dir(UPLOAD_DIR)) {
mkdir(UPLOAD_DIR, 0755, true);
}
// 5. 移動檔案
if (!move_uploaded_file($_FILES[$field]['tmp_name'], $destPath)) {
$response['message'] = '檔案移動失敗,請檢查權限';
json_exit($response);
}
// 6. 可選:壓縮/裁剪/水印處理...
// require 'ThumbnailImage.php'; // 你的縮圖類
// 7. 成功回應
$response['success'] = true;
$response['message'] = '上傳成功';
$response['path'] = '/uploads/avatars/' . $safeName; // 前端可直接用此 URL
json_exit($response);
function json_exit(array $data): never {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
?>
| 指令 | 建議值 | 說明 |
|---|---|---|
file_uploads | On | 啟用 HTTP 上傳 |
upload_max_filesize | 10M | 單一檔案上限 |
post_max_size | 12M | POST 總資料上限,必須 > upload_max_filesize |
memory_limit | 128M | 腳本記憶體上限,必須 > post_max_size |
max_execution_time | 30 | 腳本執行秒數 (大檔上傳需調大) |
max_input_time | 60 | 接收 POST 資料秒數 |
upload_tmp_dir | /var/tmp/php_upload | 指定專用暫存目錄,確保權限正確 |
finfo + getimagesize() 雙重驗證檔案真實類型php_flag engine off (Apache) 或 Nginx location ~ \.php$ { deny all; } 禁止腳本執行超過 100MB 建議採用前端分片(如 Uppy、Dropzone、WebUploader)+ 後端合併策略,避免單次請求超時與記憶體壓力。
檔案上傳看似簡單,實則涉及驗證、儲存、權限、效能多個面向。採用「白名單驗證 + 隨機命名 + 執行權限隔離 + 結構化錯誤回應」的標準化流程,能有效阻擋 99% 的上傳型攻擊並提升維護性。