| 風險點 | 當前代碼 | 加固方案 |
| 擴展名繞過 | 僅檢查擴展名字符串 | 必須 使用 finfo_file() 檢測真實 MIME;雙重驗證。 |
| 文件名注入 | 直接使用 $this->uploadx_name . '.' . $ext | 文件名白名單字符 ([a-zA-Z0-9_-]);或強制 UUID/時間戳命名。 |
| 目錄遍歷 | mkdirs 拼接 DOCUMENT_ROOT | 規範化路徑 realpath();確保最終路徑在允許根目錄內。 |
| 權限過寬 | chmod 0777 / mkdir 0777 | 目錄 0755,文件 0644;Web 服務器用戶組最小權限。 |
| 大小單位混淆 | 配置 1024 實為 KB,代碼 round(bytes/1024) | 統一使用位元組;常量 MAX_SIZE = 1024 * 1024 (1MB)。 |
| 覆蓋風險 | 同名文件直接覆蓋 | 生成唯一名 (UUID/時間戳+隨機數);或檢查存在則重命名。 |
| 類型混淆 | uploadx_type 字符串匹配 | 改為數組 ['jpg','png',...],嚴格 in_array。 |
現代化重構方向 (PSR-12 / PHP 8.1+)
declare(strict_types=1);
namespace App\Storage;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints as Assert;
class FileUploader {
private string $targetDirectory;
private array $allowedMimeTypes;
private int $maxSizeBytes;
private ?string $customNameGenerator = null;
public function __construct(
string $targetDirectory,
array $allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'],
int $maxSizeBytes = 2_000_000
) {
$this->targetDirectory = rtrim($targetDirectory, '/');
$this->allowedMimeTypes = $allowedMimeTypes;
$this->maxSizeBytes = $maxSizeBytes;
}
public function setNameGenerator(callable $generator): self {
$this->customNameGenerator = $generator;
return $this;
}
public function upload(UploadedFile $file): UploadResult {
// 1. 原生錯誤碼
if ($file->getError() !== UPLOAD_ERR_OK) {
throw new UploadException($file->getErrorMessage(), $file->getError());
}
// 2. 真實 MIME 驗證
$mime = $file->getMimeType();
if (!in_array($mime, $this->allowedMimeTypes, true)) {
throw new UploadException("MIME 類型不允許: $mime");
}
// 3. 大小驗證
if ($file->getSize() > $this->maxSizeBytes) {
throw new UploadException("文件過大: {$file->getSize()} > {$this->maxSizeBytes}");
}
// 4. 目錄準備
if (!is_dir($this->targetDirectory)) {
mkdir($this->targetDirectory, 0755, true);
}
// 5. 生成安全文件名
$ext = $file->guessExtension() ?? 'bin';
$name = $this->customNameGenerator
? ($this->customNameGenerator)($file)
: bin2hex(random_bytes(16));
$safeName = "{$name}.{$ext}";
$targetPath = $this->targetDirectory . '/' . $safeName;
// 6. 移動 (內部已處理安全檢查)
$file->move($this->targetDirectory, $safeName);
// 7. 權限
chmod($targetPath, 0644);
return new UploadResult($safeName, $targetPath, $file->getSize(), $mime, time());
}
}
readonly class UploadResult {
public function __construct(
public string $name,
public string $path,
public int $size,
public string $mimeType,
public int $timestamp
) {}
}
總結
UploadX 類展示了經典 PHP 過程式風格的配置驅動上傳封裝,邏輯清晰、功能完整。但面對現代安全威脅 (MIME 偽造、文件名注入、權限過寬) 及 PSR/嚴格類型標準,建議遷移至基於 UploadedFile 物件、強類型、異常驅動、依賴注入友好的現代組件,或直接使用成熟庫 (如 league/flysystem, symfony/http-foundation)。
- 上一篇文章: PHP 在線圖片裁剪工具全棧實現:前端 Canvas 交互與後端 ImageMagick 處理
- 下一篇文章: Flash 多文件上傳組件整合指南:前端配置參數詳解與後端 PHP 處理邏輯
Copyright ©
程式碼之家 All Rights Reserved