HTML 表單 (multipart/form-data) → $_FILES 超全域陣列 → 伺服器臨時目錄 → 驗證/清洗 → 移動至永久存儲 → 資料庫記錄元資料 → 返回結果
<form action="upload.php" method="POST" enctype="multipart/form-data">
<!-- 限制上傳大小(瀏覽器端提示,不可信,伺服器必須再驗) -->
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2MB -->
<input type="file" name="avatar" accept="image/*">
<button type="submit">上傳</button>
</form>
enctype="multipart/form-data":必須,否則 $_FILES 為空。accept:僅客戶端過濾,不可作為安全依據。$_FILES['avatar'] 結構| 鍵名 | 說明 |
|---|---|
name | 客戶端原始檔名(含路徑風險,需 basename()) |
type | MIME 型別(瀏覽器提供,可偽造) |
tmp_name | 伺服器臨時檔案完整路徑(upload_tmp_dir 設定) |
size | 檔案大小(位元組) |
error | 錯誤代碼(0=成功,1=超過 php.ini upload_max_filesize,2=超過 MAX_FILE_SIZE,3=部分上傳,4=未上傳檔案) |
if ($_FILES['avatar']['error'] !== UPLOAD_ERR_OK) throw new Exception('上傳失敗');if ($_FILES['avatar']['size'] > 2 * 1024 * 1024) throw new Exception('檔案過大');$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!in_array($_FILES['avatar']['type'], $allowed)) throw new Exception('型別不允許');$finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($_FILES['avatar']['tmp_name']); if (!in_array($mime, $allowed)) throw new Exception('內容偽裝');$img = imagecreatefromstring(file_get_contents($tmp)); imagejpeg($img, $dest); imagedestroy($img); —— 徹底剝離 EXIF、PHP 代碼注入風險。$ext = pathinfo($name, PATHINFO_EXTENSION); $newName = bin2hex(random_bytes(16)) . '.' . $ext; 防止覆蓋、路徑遍歷、執行腳本。public/ 外,或 Nginx 設定 location ~ \.php$ { deny all; }。$destDir = __DIR__ . '/../storage/uploads/' . date('Y/m/d');
if (!is_dir($destDir)) mkdir($destDir, 0755, true);
$destPath = $destDir . '/' . $newName;
if (!move_uploaded_file($_FILES['avatar']['tmp_name'], $destPath)) {
throw new Exception('儲存失敗');
}
// 寫入資料庫
$stmt = $pdo->prepare("INSERT INTO uploads (original_name, stored_path, mime, size, user_id, created_at) VALUES (?, ?, ?, ?, ?, NOW())");
$stmt->execute([$originalName, $destPath, $mime, $size, $userId]);
| 坑點 | 後果 | 修正 |
|---|---|---|
直接用 $_FILES['avatar']['name'] 當檔名 | 覆蓋、路徑遍歷、腳本執行 | 隨機化 + 白名單副檔名 |
信任 type 忽略 finfo | 上傳 .php.jpg 繞過驗證 | 二進制內容檢測 + 圖片重渲染 |
未設 upload_max_filesize / post_max_size | 大檔案靜默失敗 | php.ini / .user.ini / Nginx client_max_body_size 三層同步 |
| 臨時檔案未清理 | 磁碟佔滿 | move_uploaded_file 成功自動刪除;失敗需手動 unlink |
spark-md5 計算檔案雜湊 → 切片 → 並發上傳切片 → 合併請求。WebUploader、Uppy、Resumable.js。// 以 AWS S3 為例
$s3 = new Aws\S3\S3Client([...]);
$s3->putObject([
'Bucket' => 'my-bucket',
'Key' => "uploads/{$newName}",
'SourceFile' => $destPath,
'ACL' => 'private',
]);
// 刪除本地暫存檔
unlink($destPath);
原文代碼已重構為現代安全標準,補充 finfo 驗證、圖片重渲染、隨機命名、雲端儲存等生產級實踐。