文件上傳/下載是 Web 開發基礎功能,但細節決定安全與體驗。本文梳理從前端表單到後端處理的完整流程,指出原文代碼的安全隱患,並給出加固版實現。
enctype="multipart/form-data"、method="POST"、<input type="file">。file_uploads=On、upload_max_filesize、post_max_size、max_file_uploads、upload_tmp_dir 權限。<?php
// upload.php
$allowedMime = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$maxSize = 5 * 1024 * 1024; // 5MB
$uploadDir = __DIR__ . '/storage/uploads/'; // 非 Web 可訪問目錄
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
die('上傳錯誤碼:' . $file['error']);
}
if ($file['size'] > $maxSize) die('文件過大');
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExt)) die('擴展名不允許');
// 檢測真實 MIME(需 fileinfo 擴展)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowedMime)) die('文件類型不允許');
// 隨機文件名防覆蓋與遍歷
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = $uploadDir . $newName;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
die('移動文件失敗');
}
echo json_encode(['success' => true, 'filename' => $newName, 'original' => $file['name']]);
exit;
}
?>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept="image/*,.pdf" required>
<button type="submit">上傳</button>
</form>
$_GET['FileName'] 拼接路徑,存在目錄遍歷漏洞(../../etc/passwd)。X-Sendfile (Apache) / X-Accel-Redirect (Nginx) 交給 Web 服務器傳輸,PHP 僅做權限校驗,避免阻塞進程。<?php
// download.php?id=uuid
$id = $_GET['id'] ?? '';
// 1. 查資料庫獲取物理路徑、原始文件名、MIME
$stmt = $pdo->prepare('SELECT path, original_name, mime FROM files WHERE id=? AND deleted=0');
$stmt->execute([$id]);
$file = $stmt->fetch();
if (!$file || !is_file($file['path'])) { http_response_code(404); exit('文件不存在'); }
// 2. 權限檢查(略)
// 3. 交給 Nginx 發送
header('X-Accel-Redirect: /protected-files/' . basename($file['path']));
header('Content-Type: ' . $file['mime']);
header('Content-Disposition: attachment; filename="' . rawurlencode($file['original_name']) . '"');
header('Content-Length: ' . filesize($file['path']));
exit;
?>
Nginx 配置:
location /protected-files/ {
internal; # 僅允許內部重定向
alias /var/www/storage/uploads/;
}
uppy/resumable.js + 後端分片合併。