圖片上傳是 Web 開發高頻功能,但也是常見攻擊面。完整實作必須涵蓋:檔案類型驗證、檔名安全化、儲存路徑隔離、權限控制、錯誤處理。本文提供一個可直接用於生產環境的參考實作 (PHP 7.4+)。
<?php
declare(strict_types=1);
class ImageUploader
{
private const ALLOWED_MIME = [
'image/jpeg' => '.jpg',
'image/png' => '.png',
'image/gif' => '.gif',
'image/webp' => '.webp',
];
private const MAX_SIZE = 5 * 1024 * 1024; // 5MB
private const UPLOAD_DIR = __DIR__ . '/../storage/uploads/';
public function upload(array $file): array
{
// 1. 基礎檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('上傳錯誤代碼: ' . $file['error']);
}
if ($file['size'] > self::MAX_SIZE) {
throw new RuntimeException('檔案過大,限制 ' . (self::MAX_SIZE/1024/1024) . 'MB');
}
// 2. MIME 類型驗證 (使用 finfo 避免偽造 $_FILES['type'])
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!isset(self::ALLOWED_MIME[$mime])) {
throw new RuntimeException('不支援的檔案類型: ' . $mime);
}
$ext = self::ALLOWED_MIME[$mime];
// 3. 進階內容驗證 (可選:getimagesize 檢查圖片完整性)
$imgInfo = @getimagesize($file['tmp_name']);
if ($imgInfo === false) {
throw new RuntimeException('非有效圖片檔案或損壞');
}
// 4. 產生安全檔名 (時間戳 + 隨機字串 + 正確副檔名)
$filename = sprintf(
'%s_%s%s',
date('YmdHis'),
bin2hex(random_bytes(8)),
$ext
);
$targetPath = self::UPLOAD_DIR . $filename;
// 5. 確保目錄存在與權限
if (!is_dir(self::UPLOAD_DIR)) {
mkdir(self::UPLOAD_DIR, 0755, true);
}
// 6. 移動檔案 (原子操作)
if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
throw new RuntimeException('檔案移動失敗,請檢查權限');
}
// 7. 設定檔案權限 (僅擁有者可讀寫)
chmod($targetPath, 0644);
return [
'success' => true,
'filename' => $filename,
'path' => $targetPath,
'url' => '/uploads/' . $filename, // 相對 Web 根目錄 URL
'mime' => $mime,
'size' => $file['size'],
'width' => $imgInfo[0],
'height' => $imgInfo[1],
];
}
public function delete(string $filename): bool
{
// 防止路徑遍歷攻擊
$filename = basename($filename);
$path = self::UPLOAD_DIR . $filename;
if (!file_exists($path)) {
return false; // 或拋出異常
}
return unlink($path);
}
}
// 使用範例 (控制器中)
try {
$uploader = new ImageUploader();
$result = $uploader->upload($_FILES['img'] ?? []);
// 將 $result['filename'] 存入資料庫
echo json_encode(['code' => 0, 'data' => $result]);
} catch (Throwable $e) {
http_response_code(400);
echo json_encode(['code' => 1, 'msg' => $e->getMessage()]);
}
$_FILES['type']瀏覽器根據副檔名猜測 MIME,極易偽造。必須使用 finfo 或 getimagesize 分析檔案實際內容。
../、特殊字元、中文編碼問題)日期_隨機字串.副檔名 格式,避免衝突且不可預測public/webroot 之外 (如 storage/uploads),透過 PHP 輸出或 Nginx X-Accel-Redirect 讀取location ~ \.php$ { deny all; } 禁止腳本執行同時配置 php.ini:
upload_max_filesize = 10M post_max_size = 12M max_file_uploads = 20 max_execution_time = 30 memory_limit = 128M
basename() 移除目錄成分,確保只能刪除上傳目錄直屬檔案。
intervention/image 自動產生縮圖、水印、格式轉 WebP。StorageAdapter 介面,支援 S3、OSS、GCS 等,本地僅作暫存。attachments 表記錄檔案元數據、關聯業務模型、軟刪除標記。安全的圖片上傳不僅是「移動檔案」,而是包含內容驗證、命名策略、儲存隔離、權限控制、錯誤處理的完整防禦體系。本文提供的 ImageUploader 類別遵循單一職責原則,易於測試與擴充,可作為專案媒體管理模組的基石。記住:永遠不要信任客戶端傳來的任何資訊。