檔案上傳是 Web 應用常見功能,但也是高風險操作面。本文從表單建立、PHP 處理、驗證限制、安全儲存到防護措施,建構完整上傳解決方案。
project/
├── upload/ # 上傳儲存目錄(權限 755 或 777)
├── form.html # 上傳表單
└── upload.php # 處理邏輯
關鍵屬性:enctype="multipart/form-data"、method="post"、input type="file"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>檔案上傳</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">選擇檔案:</label>
<input type="file" name="file" id="file" required><br><br>
<input type="submit" name="submit" value="上傳">
</form>
</body>
</html>
<?php
if ($_FILES["file"]["error"] > 0) {
echo "錯誤代碼: " . $_FILES["file"]["error"] . "<br>";
} else {
echo "檔案名: " . $_FILES["file"]["name"] . "<br>";
echo "類型: " . $_FILES["file"]["type"] . "<br>";
echo "大小: " . ($_FILES["file"]["size"] / 1024) . " kB<br>";
echo "暫存路徑: " . $_FILES["file"]["tmp_name"] . "<br>";
}
?>
| 鍵名 | 說明 |
|---|---|
| name | 原始檔案名稱 |
| type | MIME 類型(瀏覽器提供,不可全信) |
| size | 檔案大小(位元組) |
| tmp_name | 伺服器暫存路徑 |
| error | 錯誤代碼(0 為成功) |
| 代碼 | 常數 | 說明 |
|---|---|---|
| 1 | UPLOAD_ERR_INI_SIZE | 超過 php.ini upload_max_filesize |
| 2 | UPLOAD_ERR_FORM_SIZE | 超過表單 MAX_FILE_SIZE |
| 3 | UPLOAD_ERR_PARTIAL | 僅部分上傳 |
| 4 | UPLOAD_ERR_NO_FILE | 未選擇檔案 |
| 6 | UPLOAD_ERR_NO_TMP_DIR | 缺少暫存目錄 |
| 7 | UPLOAD_ERR_CANT_WRITE | 寫入失敗 |
| 8 | UPLOAD_ERR_EXTENSION | 擴展阻止上傳 |
<?php
// 設定
$allowedExts = ['gif', 'jpeg', 'jpg', 'png'];
$allowedMimes = [
'image/gif', 'image/jpeg', 'image/pjpeg',
'image/x-png', 'image/png'
];
$maxSize = 200 * 1024; // 200 KB
$uploadDir = __DIR__ . '/upload/';
// 確保目錄存在
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
// 1. 檢查上傳錯誤
if ($file['error'] !== UPLOAD_ERR_OK) {
die("上傳錯誤: {$file['error']}");
}
// 2. 驗證副檔名
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
die("不允許的副檔名: $ext");
}
// 3. 驗證 MIME 類型(需配合 finfo 實際檢測)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedMimes)) {
die("不允許的檔案類型: $mime");
}
// 4. 驗證大小
if ($file['size'] > $maxSize) {
die("檔案過大,限制 {$maxSize} 位元組");
}
// 5. 防止檔名衝突:生成唯一檔名
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = $uploadDir . $newName;
// 6. 移動檔案
if (move_uploaded_file($file['tmp_name'], $destPath)) {
echo "上傳成功!儲存為: $newName";
echo "<br>原始檔名: " . htmlspecialchars($file['name']);
echo "<br>檔案大小: " . number_format($file['size'] / 1024, 2) . " KB";
} else {
die("檔案移動失敗,請檢查目錄權限");
}
}
?>
finfo 或 getimagesize() 實際檢測檔案內容upload_max_filesize、post_max_size;表單隱藏欄位 MAX_FILE_SIZE(僅客戶端提示)getimagesize() 回傳 false 即非有效圖片location /upload/ {
# 禁止執行 PHP 等腳本
location ~ \.php$ {
deny all;
return 403;
}
# 可選:僅允許圖片直接訪問
# location ~* \.(jpg|jpeg|png|gif)$ { allow all; }
# location / { deny all; }
}
# 禁止腳本執行
SetHandler default-handler
Options -ExecCGI
RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .php8 .phar
php_flag engine off
# 或僅允許圖片
# <FilesMatch "\.(?i:jpg|jpeg|png|gif)$">
# Allow from all
# </FilesMatch>
# <FilesMatch ".*">
# Deny from all
# </FilesMatch>
超過 PHP 限制的大檔案需前端分片(如 Plupload、Uppy、Dropzone),後端合併:
<?php
// 合併分片範例
$chunkDir = __DIR__ . '/chunks/' . $fileId . '/';
$finalPath = __DIR__ . '/upload/' . $fileName;
$chunks = glob($chunkDir . '*.part');
sort($chunks, SORT_NATURAL);
$fp = fopen($finalPath, 'wb');
foreach ($chunks as $chunk) {
fwrite($fp, file_get_contents($chunk));
}
fclose($fp);
// 清理分片
array_map('unlink', $chunks);
rmdir($chunkDir);
?>
| 設定 | 建議 | 說明 |
|---|---|---|
| file_uploads | On | 啟用上傳功能 |
| upload_max_filesize | 10M | 單檔最大值 |
| post_max_size | 12M | POST 總大小,需 > upload_max_filesize |
| max_file_uploads | 20 | 單次請求最大檔案數 |
| upload_tmp_dir | /tmp/upload | 指定暫存目錄(需可寫) |
| max_input_time | 300 | 大檔上傳需較長解析時間 |
檔案上傳看似簡單,實則涉及驗證、儲存、權限、效能多重考量。遵循「不信任輸入、最小權限、纵深防禦」原則,才能構建安全可靠的上傳功能。