首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 多檔案上傳系統完整實作與安全強化指南

     編輯:PHP 綜合知識

多檔案上傳是常見需求,但原始碼常存在安全漏洞(任意檔案上傳、路徑遍歷、型別繞過)。本文提供一個結構清晰、安全強化的多檔案上傳類別,涵蓋驗證、重命名、儲存、錯誤處理與前端整合。

核心上傳類別

<?php
class MultiFileUploader {
    private $uploadDir;
    private $allowedMimes = [];
    private $maxFileSize;      // bytes
    private $maxFiles;
    private $overwrite = false;
    private $errors = [];
    private $uploaded = [];

    public function __construct(string $uploadDir, array $options = []) {
        $this->uploadDir = rtrim($uploadDir, '/') . '/';
        $this->allowedMimes = $options['allowed_mimes'] ?? [
            'image/jpeg', 'image/png', 'image/gif', 'image/webp',
            'application/pdf', 'text/plain'
        ];
        $this->maxFileSize = $options['max_file_size'] ?? 5 * 1024 * 1024; // 5MB
        $this->maxFiles = $options['max_files'] ?? 10;
        $this->overwrite = $options['overwrite'] ?? false;

        if (!is_dir($this->uploadDir)) {
            mkdir($this->uploadDir, 0755, true);
        }
    }

    public function upload(array $files = null): array {
        $files = $files ?? $_FILES;
        $this->errors = [];
        $this->uploaded = [];

        // 正規化 $_FILES 結構
        $normalized = $this->normalizeFiles($files);
        if (count($normalized) > $this->maxFiles) {
            $this->errors[] = "超過最大檔案數限制 ({$this->maxFiles})";
            return $this->getResult();
        }

        foreach ($normalized as $file) {
            $this->processFile($file);
        }
        return $this->getResult();
    }

    private function normalizeFiles(array $files): array {
        $result = [];
        foreach ($files as $name => $data) {
            if (isset($data['tmp_name'])) {
                // 單檔案結構
                if (is_array($data['tmp_name'])) {
                    // 多檔案同名輸入 
                    foreach ($data['tmp_name'] as $i => $tmp) {
                        if ($tmp !== '' && $data['error'][$i] === UPLOAD_ERR_OK) {
                            $result[] = [
                                'name' => $data['name'][$i],
                                'type' => $data['type'][$i],
                                'tmp_name' => $tmp,
                                'size' => $data['size'][$i],
                                'error' => $data['error'][$i]
                            ];
                        }
                    }
                } else {
                    // 單檔案
                    if ($data['tmp_name'] !== '' && $data['error'] === UPLOAD_ERR_OK) {
                        $result[] = $data;
                    }
                }
            }
        }
        return $result;
    }

    private function processFile(array $file) {
        // 1. MIME 驗證(建議配合 finfo 二次驗證)
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $realMime = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);

        if (!in_array($realMime, $this->allowedMimes, true)) {
            $this->errors[] = "檔案 {$file['name']} 類型不允許: $realMime";
            return;
        }

        // 2. 大小檢查
        if ($file['size'] > $this->maxFileSize) {
            $this->errors[] = "檔案 {$file['name']} 超過大小限制 (" . $this->formatBytes($this->maxFileSize) . ")";
            return;
        }

        // 3. 產生安全檔名
        $ext = $this->getSafeExtension($realMime, $file['name']);
        $newName = $this->generateUniqueName($ext);

        $destPath = $this->uploadDir . $newName;

        // 4. 覆蓋檢查
        if (file_exists($destPath) && !$this->overwrite) {
            $this->errors[] = "檔案 {$newName} 已存在,略過";
            return;
        }

        // 5. 移動檔案
        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            $this->errors[] = "檔案 {$file['name']} 移動失敗";
            return;
        }

        // 6. 設定權限
        chmod($destPath, 0644);

        $this->uploaded[] = [
            'original_name' => $file['name'],
            'saved_name' => $newName,
            'path' => $destPath,
            'size' => $file['size'],
            'mime' => $realMime,
            'url' => $this->getPublicUrl($newName)
        ];
    }

    private function getSafeExtension(string $mime, string $originalName): string {
        $map = [
            'image/jpeg' => 'jpg', 'image/png' => 'png',
            'image/gif' => 'gif', 'image/webp' => 'webp',
            'application/pdf' => 'pdf', 'text/plain' => 'txt'
        ];
        return $map[$mime] ?? pathinfo($originalName, PATHINFO_EXTENSION) ?? 'bin';
    }

    private function generateUniqueName(string $ext): string {
        do {
            $name = bin2hex(random_bytes(16)) . '.' . $ext;
        } while (file_exists($this->uploadDir . $name));
        return $name;
    }

    private function formatBytes(int $bytes): string {
        $units = ['B','KB','MB','GB'];
        for ($i = 0; $bytes >= 1024 && $i < 3; $i++) $bytes /= 1024;
        return round($bytes, 2) . ' ' . $units[$i];
    }

    private function getPublicUrl(string $filename): string {
        // 根據實際部署調整
        return '/uploads/' . $filename;
    }

    public function getResult(): array {
        return [
            'success' => empty($this->errors),
            'uploaded' => $this->uploaded,
            'errors' => $this->errors
        ];
    }
}

前端表單範例

<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="hidden" name="MAX_FILE_SIZE" value="5242880">
    <input type="file" name="files[]" multiple accept="image/*,.pdf,.txt">
    <label><input type="checkbox" name="overwrite"> 覆蓋同名檔案</label>
    <button type="submit">上傳</button>
</form>

後端處理腳本 (upload.php)

<?php
require 'MultiFileUploader.php';

$uploader = new MultiFileUploader(__DIR__ . '/uploads', [
    'allowed_mimes' => ['image/jpeg','image/png','image/gif','image/webp','application/pdf'],
    'max_file_size' => 10 * 1024 * 1024,
    'max_files' => 20,
    'overwrite' => isset($_POST['overwrite'])
]);

$result = $uploader->upload();

header('Content-Type: application/json');
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

安全強化檢查清單

風險防護措施
惡意腳本上傳MIME 白名單 + finfo 實際檢測 + 副檔名映射 + 儲存目錄禁用執行權限
檔名路徑遍歷忽略原始檔名,使用隨機雜湊檔名
大檔案阻塞服務MAX_FILE_SIZE、upload_max_filesize、post_max_size、max_file_size 多層限制
併發檔名衝突random_bytes(16) + 存在性檢查迴圈
上傳目錄洩漏儲存路徑置於 Web Root 外,或 Nginx location /uploads { internal; }
CSRF 攻擊表單加入 CSRF Token 驗證
殭屍暫存檔定期清理 sys_get_temp_dir() 超過 24 小時的 php* 暫存檔

進階擴展建議

  • 分塊上傳:大檔案(>100MB)採用 plupload 或 uppy 前端分片,後端合併
  • 雲端儲存:介面對接 S3、GCS、Azure Blob、阿里云 OSS,本地暫存後非同步上傳
  • 圖片處理管線:上傳後觸發 Worker 生成縮圖、WebP 轉換、水印、EXIF 剝離
  • 病毒掃描:整合 ClamAV 掃描後再移至永久儲存
  • 資料庫記錄:上傳成功寫入 uploads 表,關聯業務實體(文章、用戶、訂單)

採用物件導向封裝上傳邏輯,能集中管理安全策略、便於單元測試、支援多種儲存後端替換。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved