檔案上傳是 Web 應用高風險功能,處理不當易導致惡意腳本上傳、任意程式碼執行。本文提供一套完整的安全上傳流程:MIME 類型驗證、副檔名白名單、檔案重命名、儲存目錄權限控制、防止直接存取執行。
<?php
// upload.php
$uploadDir = __DIR__ . '/uploads/';
$allowedMimes = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
];
$maxSize = 5 * 1024 * 1024; // 5MB
// 確保上傳目錄存在且不可執行 PHP
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
// 建立 .htaccess 禁止腳本執行(Apache)
file_put_contents($uploadDir . '.htaccess', "SetHandler default-handler\nOptions -ExecCGI\nphp_flag engine off\n");
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];
// 1. 檢查上傳錯誤
if ($file['error'] !== UPLOAD_ERR_OK) {
die('上傳錯誤代碼: ' . $file['error']);
}
// 2. 檢查檔案大小
if ($file['size'] > $maxSize) {
die('檔案過大,限制 ' . ($maxSize / 1024 / 1024) . 'MB');
}
// 3. 偵測真實 MIME 類型(使用 finfo,不可信任 $_FILES['type'])
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!isset($allowedMimes[$mime])) {
die('不支援的檔案類型: ' . $mime);
}
// 4. 產生安全檔名:隨機字串 + 正確副檔名
$ext = $allowedMimes[$mime];
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = $uploadDir . $safeName;
// 5. 移動檔案
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
die('檔案移動失敗');
}
// 6. 設定檔案權限(僅讀)
chmod($destPath, 0644);
// 成功回應
echo json_encode([
'success' => true,
'filename' => $safeName,
'original' => $file['name'],
'url' => '/uploads/' . $safeName,
]);
exit;
}
?>
<!DOCTYPE html>
<html>
<head><title>安全上傳示範</title></head>
<body>
<form method="post" enctype="multipart/form-data">
<input type="file" name="file" accept="image/*" required>
<button type="submit">上傳</button>
</form>
</body>
</html>
| 措施 | 說明 |
|---|---|
使用 finfo 偵測 MIME | 依檔案內容判斷類型,防止偽造 $_FILES['type'] |
| 副檔名白名單映射 | 僅允許已知安全的副檔名,拒絕 .php, .phtml, .php5 等 |
| 隨機檔名 | 避免檔名衝突、路徑遍歷、使用者推測檔案位置 |
| 儲存目錄禁用執行 | Apache 用 .htaccess php_flag engine off;Nginx 設定 location ~ \.php$ { deny all; } |
| 檔案權限 0644 | 移除執行權限,降低風險 |
| 限制上傳大小 | 防止 DoS 佔滿磁碟,配合 php.ini upload_max_filesize, post_max_size |
<?php
// 上傳為圖片後,用 GD 重新繪製儲存,可剝離夾帶的 PHP 代碼
$img = imagecreatefromstring(file_get_contents($destPath));
if ($img !== false) {
imagejpeg($img, $destPath, 90); // 重寫為純淨 JPEG
imagedestroy($img);
}
?>
location /uploads/ {
# 禁止執行任何腳本
location ~ \.php$ {
return 403;
}
# 禁止解析雙副檔名
location ~ \.(php|phtml|php5)\. {
return 403;
}
}
安全上傳的核心原則:不信任客戶端任何資訊(檔名、MIME、大小),伺服器端完整驗證,儲存與執行環境隔離。上述措施疊加使用,可有效阻擋 99% 以上的上傳攻擊。