檔案上傳是 Web 開發基礎功能,但也是安全漏洞高發區。本文系統性說明表單設計、後端處理、安全驗證、錯誤處理與進階優化,提供可直接用於生產環境的完整範例。
<form action="upload.php" method="post" enctype="multipart/form-data">
<!-- 必須:啟用檔案上傳編碼 -->
<input type="hidden" name="MAX_FILE_SIZE" value="5242880"> <!-- 5MB,客戶端提示用 -->
<input type="file" name="myfile" accept="image/*,.pdf" required>
<button type="submit">上傳</button>
</form>
enctype="multipart/form-data":缺少會導致 $_FILES 為空MAX_FILE_SIZE:瀏覽器端軟性限制,不可信任,後端必須再驗證accept:提示瀏覽器過濾檔案類型,非安全邊界<?php
function handleUpload(array $file, array $options = []): array {
// 預設配置
$config = array_merge([
'upload_dir' => __DIR__ . '/uploads/',
'allowed_mimes' => ['image/jpeg','image/png','image/gif','image/webp','application/pdf'],
'max_size' => 5 * 1024 * 1024, // 5MB
'overwrite' => false,
], $options);
// 1. 檢查上傳錯誤碼
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => uploadErrorMessage($file['error'])];
}
// 2. 檢查檔案大小
if ($file['size'] > $config['max_size']) {
return ['success' => false, 'error' => '檔案大小超過限制'];
}
// 3. MIME 型別二次驗證 (防偽造)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$realMime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($realMime, $config['allowed_mimes'], true)) {
return ['success' => false, 'error' => "不允許的檔案類型: $realMime"];
}
// 4. 產生安全儲存檔名
$ext = getExtensionFromMime($realMime);
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = $config['upload_dir'] . $filename;
// 5. 確保目錄存在
if (!is_dir($config['upload_dir'])) {
mkdir($config['upload_dir'], 0755, true);
}
// 6. 覆蓋檢查
if (file_exists($destPath) && !$config['overwrite']) {
return ['success' => false, 'error' => '檔案已存在'];
}
// 7. 移動檔案 (關鍵:使用 move_uploaded_file 確保來源是合法上傳檔)
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
return ['success' => false, 'error' => '檔案儲存失敗'];
}
// 8. 設定權限
chmod($destPath, 0644);
return [
'success' => true,
'filename' => $filename,
'original_name' => $file['name'],
'path' => $destPath,
'size' => $file['size'],
'mime' => $realMime,
'url' => '/uploads/' . $filename // 根據實際 Web 路徑調整
];
}
function uploadErrorMessage(int $code): string {
return match ($code) {
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => '檔案超過大小限制',
UPLOAD_ERR_PARTIAL => '檔案上傳不完整',
UPLOAD_ERR_NO_FILE => '未選擇檔案',
UPLOAD_ERR_NO_TMP_DIR => '伺服器缺少暫存目錄',
UPLOAD_ERR_CANT_WRITE => '伺服器寫入失敗',
UPLOAD_ERR_EXTENSION => 'PHP 擴展阻止上傳',
default => '未知上傳錯誤',
};
}
function getExtensionFromMime(string $mime): string {
return match ($mime) {
'image/jpeg' => 'jpg', 'image/png' => 'png',
'image/gif' => 'gif', 'image/webp' => 'webp',
'application/pdf' => 'pdf', default => 'bin'
};
}
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$result = handleUpload($_FILES['myfile']);
header('Content-Type: application/json');
echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit;
}
?>
<!-- HTML 表單 -->
| 攻擊向量 | 風險 | 防護措施 |
|---|---|---|
| 惡意腳本上傳 (.php, .phtml) | 遠端程式碼執行 (RCE) | MIME 白名單 + finfo 驗證 + 副檔名映射 + 儲存目錄禁用 PHP 解析 |
| 檔名路徑遍歷 (../../etc/passwd) | 任意檔案寫入 | 忽略原始檔名,使用隨機雜湊檔名 |
| MIME 偽造 (Content-Type: image/jpeg 實為 PHP) | 繞過型別檢查 | 伺服器端 finfo_file() 實際檢測檔案魔數 |
| 大檔案 DoS | 佔滿磁碟/記憶體 | 多層大小限制:php.ini、MAX_FILE_SIZE、程式碼驗證 |
| 併發檔名衝突 | 覆蓋他人檔案 | random_bytes(16) + 存在性檢查 |
| 暫存檔殘留 | 資訊洩漏 | move_uploaded_file 會自動清理,異常時註冊 register_shutdown_function 清理 |
; php.ini
file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M ; 必須 > upload_max_filesize
max_file_uploads = 20
max_input_time = 60
memory_limit = 128M
; Nginx: 禁用上傳目錄腳本執行
location /uploads/ {
location ~ \.php$ { return 403; }
expires 30d;
add_header X-Content-Type-Options nosniff;
}
; Apache .htaccess (uploads/)
Require all denied
Options -ExecCGI
AddHandler cgi-script .php .phtml .php3 .php4 .php5 .php7 .phps
Intervention/Image 生成縮圖、WebP、水印、EXIF 移除Uppy / Dropzone 前端分片,後端合併 (臨時目錄 + 合併腳本)StorageAdapter,實作 LocalStorage, S3Storage, OSSStorageclamscan 非同步掃描,通過才移至永久區uploads 表關聯業務實體,支援軟刪除、版本控制$_FILES 為空 → 檢查 enctype、post_max_size、file_uploads=Ontmp_name 做任何讀寫操作、move_uploaded_file 只呼叫一次遵循「不信任客戶端、白名單驗證、隨機化儲存、權限最小化」原則,即可構建安全可靠的檔案上傳功能。