萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> PHP 程式設計

PHP 安全文件上傳最佳實踐:從基礎驗證到防禦攻擊

為什麼需要「不只是一行代碼」的上傳處理?

move_uploaded_file() 雖然能一行完成移動,但直接使用無異於裸奔。生產環境必須處理:文件類型偽裝、大小超限、惡意腳本執行、文件名衝突、目錄遍歷 等風險。本文提供一個可直接落地的安全上傳範例。

完整安全上傳代碼

<?php
/**
 * 安全文件上傳處理器
 * @param string $inputName  表單 file 控件 name 屬性
 * @param array  $config     配置陣列
 * @return array|false       成功返回文件信息,失敗返回 false
 */
function secure_upload(string $inputName, array $config = []): array|false {
    // 1. 合並預設配置
    $defaults = [
        'allowedTypes' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx'],
        'maxSize'      => 2 * 1024 * 1024,          // 2MB
        'uploadDir'    => __DIR__ . '/uploads/',    // 絕對路徑
        'prefix'       => 'upload_',                // 文件名前綴
    ];
    $cfg = array_merge($defaults, $config);

    // 2. 基礎檢查
    if (empty($_FILES[$inputName]) || $_FILES[$inputName]['error'] !== UPLOAD_ERR_OK) {
        return false; // 無文件或上傳出錯
    }
    $file = $_FILES[$inputName];

    // 3. 文件大小驗證(雙重保障:客戶端 MAX_FILE_SIZE + 服務端)
    if ($file['size'] > $cfg['maxSize']) {
        throw new Exception("文件大小超過限制 ({$cfg['maxSize']} 字節)");
    }

    // 4. 文件類型驗證:僅信任副檔名不夠,需結合 MIME 類型與魔術字節
    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, $cfg['allowedTypes'], true)) {
        throw new Exception("不允許的文件類型: {$ext}");
    }

    // 進階:檢測真實 MIME 類型(需啟用 fileinfo 擴展)
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime  = $finfo->file($file['tmp_name']);
    $allowedMimes = [
        'jpg'  => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png'  => 'image/png',
        'gif'  => 'image/gif',
        'webp' => 'image/webp',
        'pdf'  => 'application/pdf',
        'doc'  => 'application/msword',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    ];
    if (!isset($allowedMimes[$ext]) || $allowedMimes[$ext] !== $mime) {
        throw new Exception("文件內容與副檔名不符,疑似偽裝文件");
    }

    // 5. 確保上傳目錄存在且不可執行腳本
    if (!is_dir($cfg['uploadDir'])) {
        mkdir($cfg['uploadDir'], 0755, true);
        // 寫入 .htaccess 禁止腳本執行(Apache)
        file_put_contents($cfg['uploadDir'] . '.htaccess', "SetHandler default-handler\nOptions -ExecCGI");
    }

    // 6. 生成唯一安全文件名:前綴 + 時間戳 + 隨機字符 + 副檔名
    $safeName = $cfg['prefix'] . time() . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
    $destPath = $cfg['uploadDir'] . $safeName;

    // 7. 移動文件(原子操作,跨分區安全)
    if (!move_uploaded_file($file['tmp_name'], $destPath)) {
        throw new Exception("文件移動失敗,請檢查目錄權限");
    }

    // 8. 設置文件權限:僅可讀寫,不可執行
    chmod($destPath, 0644);

    // 9. 返回標準化信息
    return [
        'original_name' => $file['name'],
        'saved_name'    => $safeName,
        'path'          => $destPath,
        'url'           => str_replace($_SERVER['DOCUMENT_ROOT'], '', $destPath), // 相對 Web 根目錄 URL
        'size'          => $file['size'],
        'mime'          => $mime,
        'ext'           => $ext,
    ];
}

// ===== 使用示例 =====
try {
    $result = secure_upload('resumefile', [
        'allowedTypes' => ['jpg', 'png', 'gif', 'doc', 'docx', 'pdf'],
        'maxSize'      => 1024 * 1024, // 1MB
        'uploadDir'    => __DIR__ . '/upload/resume/',
    ]);
    if ($result) {
        // $result['url'] 可直接存入數據庫
        echo "上傳成功: {$result['url']}";
    }
} catch (Exception $e) {
    // 統一錯誤處理:記錄日誌、返回友好提示
    error_log("上傳失敗: " . $e->getMessage());
    echo "上傳失敗: " . $e->getMessage();
}

關鍵安全點深度解析

風險點防禦措施代碼體現
副檔名偽裝 (.php.jpg)白名單驗證副檔名 + MIME 類型雙重校驗步驟 4
大文件撐爆磁盤服務端強制限制 $maxSize步驟 3
文件名衝突覆蓋時間戳 + 隨機字節生成唯一名步驟 6
上傳目錄執行腳本.htaccess 禁用執行權限 + chmod 0644步驟 5、7
路徑遍歷攻擊basename() + 固定上傳目錄,不使用用戶輸入路徑步驟 6
MIME 偽造finfo 魔術字節檢測真實類型步驟 4

生產環境擴展建議

  • 異步處理:大文件使用分片上傳(Plupload/Uppy)+ 消息隊列異步處理。
  • 雲存儲直傳:簽名直傳至 OSS/S3,減輕應用服務器帶寬壓力。
  • 病毒掃描:集成 ClamAV 或雲端安全服務掃描上傳文件。
  • 圖片處理:上傳後自動壓縮、生成縮略圖、去除 EXIF 敏感信息。
  • 審計日誌:記錄上傳者 IP、用戶 ID、文件雜湊、操作時間。

總結

安全文件上傳的核心原則是 「不信任任何客戶端數據」:副檔名可偽造、MIME 可偽造、文件名可攜帶路徑。通過 白名單機制、魔術字節驗證、隨機重命名、執行權限隔離 四層防線,可有效阻斷 99% 以上的上傳型攻擊。

copyright © 萬盛學電腦網 all rights reserved