PHP 檔案上傳機制仰賴兩個關鍵函數,理解其差異與正確用法是建立安全上傳功能的基礎。
用途:驗證檔案確實是透過 HTTP POST 上傳,而非本地檔案路徑偽造。
bool is_uploaded_file(string $filename)
回傳:成功上傳回 true,否則 false。
必要性:防止攻擊者利用變數覆蓋漏洞,偽造 $_FILES 內容竊取伺服器敏感檔案(如 /etc/passwd)。
用途:將上傳的暫存檔移動到指定永久位置。
bool move_uploaded_file(string $filename, string $destination)
特性:
<form enctype="multipart/form-data" method="post" action="upload.php">
<input type="hidden" name="MAX_FILE_SIZE" value="2000000">
<input type="file" name="upfile">
<input type="submit" value="上傳">
</form>
enctype="multipart/form-data":必要,否則 $_FILES 為空MAX_FILE_SIZE:客戶端提示上限(瀏覽器可能忽略),伺服端仍需驗證type="file":啟用檔案選擇對話框| 鍵名 | 說明 | 範例 |
|---|---|---|
| name | 客戶端原始檔名 | photo.jpg |
| type | 瀏覽器提供的 MIME 類型 | image/jpeg |
| size | 檔案大小(位元組) | 2048576 |
| tmp_name | 伺服器暫存路徑 | /tmp/phpA1b2C3 |
| error | 錯誤代碼(0=成功) | 0 |
<?php
// FileUploader.php
class FileUploader {
private $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
private $max_size = 5 * 1024 * 1024; // 5MB
private $upload_dir;
public function __construct(string $upload_dir) {
$this->upload_dir = rtrim($upload_dir, '/') . '/';
if (!is_dir($this->upload_dir)) {
mkdir($this->upload_dir, 0755, true);
}
}
public function upload(string $field_name): array {
if (!isset($_FILES[$field_name])) {
return ['success' => false, 'error' => '欄位不存在'];
}
$file = $_FILES[$field_name];
// 1. 基礎錯誤檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
return ['success' => false, 'error' => $this->get_error_message($file['error'])];
}
// 2. 確認合法上傳檔案
if (!is_uploaded_file($file['tmp_name'])) {
return ['success' => false, 'error' => '非法檔案來源'];
}
// 3. 大小檢查
if ($file['size'] > $this->max_size) {
return ['success' => false, 'error' => '檔案過大'];
}
// 4. MIME 類型驗證(使用 finfo 避免偽造)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $this->allowed_types)) {
return ['success' => false, 'error' => '檔案類型不允許'];
}
// 5. 產生安全檔名
$ext = $this->get_extension($mime);
$filename = bin2hex(random_bytes(16)) . ".{$ext}";
$destination = $this->upload_dir . $filename;
// 6. 移動檔案
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'error' => '檔案移動失敗'];
}
// 7. 設定安全權限
chmod($destination, 0644);
return [
'success' => true,
'filename' => $filename,
'path' => $destination,
'original_name' => $file['name'],
'size' => $file['size'],
'mime' => $mime
];
}
private function get_extension(string $mime): string {
$map = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'application/pdf' => 'pdf',
];
return $map[$mime] ?? 'bin';
}
private function get_error_message(int $code): string {
$messages = [
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 => '寫入磁碟失敗',
UPLOAD_ERR_EXTENSION => 'PHP 擴充套件中斷上傳',
];
return $messages[$code] ?? '未知上傳錯誤';
}
}
// 使用範例
$uploader = new FileUploader(__DIR__ . '/uploads');
$result = $uploader->upload('upfile');
if ($result['success']) {
echo "上傳成功:{$result['path']}";
} else {
echo "失敗:{$result['error']}";
}
?>
<!-- upload_form.php -->
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>檔案上傳</title></head>
<body>
<h2>檔案上傳</h2>
<form action="save_upload.php" method="post" enctype="multipart/form-data">
<label>選擇檔案:<input type="file" name="upfile" required></label>
<button type="submit">上傳</button>
</form>
</body></html>
<?php
// save_upload.php
require_once 'FileUploader.php';
$uploader = new FileUploader(__DIR__ . '/uploads');
$result = $uploader->upload('upfile');
header('Content-Type: application/json');
echo json_encode($result);
?>
is_uploaded_file() 或 move_uploaded_file() 驗證來源finfo 驗證真實 MIME 類型,不要信任 $_FILES['type']