<form action="upload.php" method="post" enctype="multipart/form-data"> <input type="hidden" name="MAX_FILE_SIZE" value="1048576"> <!-- 客戶端提示,非強制 --> <input type="file" name="upfile" accept="image/*"> <button type="submit">上傳</button> </form>
upload.php(完整防護)<?php
// 配置
const UPLOAD_DIR = __DIR__ . '/uploads/';
const MAX_SIZE = 1024 * 1024; // 1MB
const ALLOWED_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
// 確保目錄存在且不可執行
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
file_put_contents(UPLOAD_DIR . '.htaccess', 'SetHandler default-handler'); // Apache 禁用 PHP 解析
// 檢查錯誤碼
if (!isset($_FILES['upfile']) || $_FILES['upfile']['error'] !== UPLOAD_ERR_OK) {
$msg = match ($_FILES['upfile']['error'] ?? UPLOAD_ERR_NO_FILE) {
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => '文件超過大小限制',
UPLOAD_ERR_PARTIAL => '文件上傳不完整',
UPLOAD_ERR_NO_FILE => '未選擇文件',
default => '上傳失敗,錯誤碼: ' . $_FILES['upfile']['error'],
};
die(json_encode(['code'=>1, 'msg'=>$msg]));
}
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['upfile']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ALLOWED_MIME, true)) {
die(json_encode(['code'=>1, 'msg'=>'文件類型不允許: ' . $mime]));
}
$ext = strtolower(pathinfo($_FILES['upfile']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ALLOWED_EXT, true)) {
die(json_encode(['code'=>1, 'msg'=>'副檔名不允許']));
}
if ($_FILES['upfile']['size'] > MAX_SIZE) {
die(json_encode(['code'=>1, 'msg'=>'文件體積超過 1MB']));
}
// 生成唯一文件名:時間戳 + 隨機字符 + 原副檔名
$newName = date('YmdHis') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$dest = UPLOAD_DIR . $newName;
if (!move_uploaded_file($_FILES['upfile']['tmp_name'], $dest)) {
die(json_encode(['code'=>1, 'msg'=>'移動文件失敗,請檢查目錄權限']));
}
// 寫入數據庫記錄(PDO)
require_once 'db.php';
$stmt = $pdo->prepare('INSERT INTO uploads (original_name, stored_name, mime, size, created_at) VALUES (?, ?, ?, ?, NOW())');
$stmt->execute([$_FILES['upfile']['name'], $newName, $mime, $_FILES['upfile']['size']]);
echo json_encode(['code'=>0, 'msg'=>'上傳成功', 'url'=>'/uploads/' . $newName]);
?>
| 風險 | 對策 |
|---|---|
| 偽裝腳本上傳 | 雙重驗證:finfo_file 檢測真實 MIME + 副檔名白名單 |
| 文件名遍歷/覆蓋 | 重命名存儲,不信任用戶文件名 |
| 上傳目錄執行腳本 | .htaccess / Nginx location ~ \.php$ { deny all; } |
| 大文件撐爆磁碟 | MAX_FILE_SIZE + 服務端 $_FILES['size'] 雙重限制 |
| 併發衝突 | 隨機文件名幾乎無衝突;極端情況可加 while(file_exists) rename |
前端使用 Plupload/Uppy 切片,後端臨時目錄收集分片,全部分片到齊後合併、驗證、入庫。超出本文範圍,另文詳述。