文件上傳是 Web 開發的基礎功能,但也是安全漏洞高發區。本文提供一個完整、可直接運行的單文件上傳示例,涵蓋表單構建、多重安全驗證、文件存儲、已上傳文件列表展示與下載鏈接生成,並詳細講解每個安全控制點。
upload.php)<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PHP 文件上傳示例</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; }
.file-list a { display: block; margin: 0.5rem 0; }
.msg { padding: 0.5rem; margin: 1rem 0; border-radius: 4px; }
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<h1>文件上傳</h1>
<!-- 上傳表單:enctype 必須為 multipart/form-data -->
<form method="post" enctype="multipart/form-data">
<label>選擇文件: <input type="file" name="upload_file" required></label>
<button type="submit">上傳</button>
</form>
<hr>
<h2>已上傳文件</h2>
<div class="file-list">
<?php
// ========== 配置區 ==========
$storeDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/'; // 存儲目錄
$maxSize = 200 * 1024 * 1024; // 200MB
$allowOverwrite = false; // 是否允許覆蓋同名文件
// ============================
// 確保目錄存在
if (!is_dir($storeDir)) {
mkdir($storeDir, 0755, true);
}
// 遞歸獲取文件列表(含子目錄)
function getFileList(string $dir): array {
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$files[] = $file->getRealPath();
}
}
return $files;
}
// 顯示文件列表
$fileList = getFileList($storeDir);
foreach ($fileList as $index => $fullPath) {
// 生成相對 Web 根目錄的 URL 路徑
$relPath = '/' . ltrim(str_replace($_SERVER['DOCUMENT_ROOT'], '', $fullPath), '/');
echo '<a href="' . htmlspecialchars($relPath) . '" target="_blank">';
echo htmlspecialchars(basename($fullPath)) . ' (' . formatBytes(filesize($fullPath)) . ')</a>';
}
function formatBytes(int $bytes): string {
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units)-1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
?>
</div>
<?php
// ========== 上傳處理邏輯(POST 請求時執行) ==========
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
$file = $_FILES['upload_file'];
// 1. 檢查上傳錯誤碼
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors = [
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 擴展阻止',
];
$msg = $errors[$file['error']] ?? '未知上傳錯誤';
echo '<div class="msg error">' . htmlspecialchars($msg) . '</div>';
} else {
// 2. 文件大小二次驗證(防範偽造請求)
if ($file['size'] > $maxSize) {
echo '<div class="msg error">文件大小超過限制 (' . formatBytes($maxSize) . ')</div>';
}
// 3. 文件名安全處理
$originalName = basename($file['name']);
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName); // 過濾危險字符
$targetPath = $storeDir . $safeName;
// 4. 重名檢查
if (file_exists($targetPath) && !$allowOverwrite) {
echo '<div class="msg error">文件已存在,不允許覆蓋</div>';
} else {
// 5. 移動文件(核心步驟:從臨時目錄移至目標目錄)
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo '<div class="msg success">上傳成功:' . htmlspecialchars($safeName) . '</div>';
// 可選:刷新頁面顯示新列表
echo '<script>setTimeout(() => location.reload(), 1000);</script>';
} else {
echo '<div class="msg error">文件移動失敗,請檢查目錄權限</div>';
}
}
}
// 顯示文件元信息(調試用)
echo '<h3>文件信息</h3><pre>';
print_r([
'name' => $file['name'],
'type' => $file['type'], // MIME 類型(瀏覽器提供,不可信)
'size' => $file['size'],
'tmp_name' => $file['tmp_name'],
'error' => $file['error'],
]);
echo '</pre>';
}
?>
</body>
</html>
enctype="multipart/form-data"缺少此屬性將導致 $_FILES 為空,文件無法上傳。
$_FILES['file']['error'] 是判斷上傳是否成功的**唯一可靠標準**,不可僅靠 file_exists() 或 filesize() 判斷。
php.ini 中 upload_max_filesize、post_max_size(需 > upload_max_filesize)。$file['size'],防範繞過客戶端限制的惡意請求。// 移除路徑遍歷風險、特殊字符
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($file['name']));
// 更安全:生成隨機文件名 + 保留副檔名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeName = uniqid('upload_') . ($ext ? '.' . $ext : '');
原則:絕不信任原始文件名,存儲時使用隨機名,原始名存入數據庫供下載時顯示。
$file['type'] 由瀏覽器根據副檔名猜測,極易偽造。真實類型檢測需用:
// 方法一:finfo(推薦,需 fileinfo 擴展)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$realMime = $finfo->file($file['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (!in_array($realMime, $allowed)) die('文件類型不允許');
// 方法二:getimagesize()(僅圖片)
$info = getimagesize($file['tmp_name']);
if ($info === false) die('非合法圖片文件');
0755(擁有者可寫,組/其他可讀執行),避免 0777。location /uploads { php_flag engine off; })。readfile())提供下載,徹底杜絕腳本執行風險。max_execution_time、memory_limit。文件上傳看似簡單,實則涉及協議解析、文件系統權限、MIME 偽造防範、存儲架構設計等多層知識。掌握 $_FILES 結構、move_uploaded_file() 原子操作、finfo 真實類型檢測、隨機文件名生成、存儲目錄隔離這五大核心技術點,即可構建安全可靠的上傳功能。實際項目中,建議封裝為可復用的 Uploader 服務類,統一錯誤處理、日志記錄、事件分發(如上傳成功觸發圖片處理隊列),提升可維護性與擴展性。