萬盛學電腦網

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

PHP 檔案上傳驗證:前後端雙重檢查與檔案類型偵測

前端檢查:用戶體驗層 (可繞過,非安全防線)

<form action="" method="post" enctype="multipart/form-data">
    檔案上傳:<input type="file" name="file" id="file">
    <input type="submit" id="send" value="提交">
</form>

<script>
document.getElementById('send').onclick = function(e) {
    const fileInput = document.getElementById('file');
    if (!fileInput.value) { // 僅檢查文件名非空
        alert('請選擇檔案');
        return false; // 阻止提交
    }
    // 可選:客戶端檢查副檔名/大小 (易偽造)
    const allowed = ['.jpg', '.jpeg', '.png', '.gif', '.pdf'];
    const ext = fileInput.value.substring(fileInput.value.lastIndexOf('.')).toLowerCase();
    if (!allowed.includes(ext)) {
        alert('僅允許圖片/PDF 檔案');
        return false;
    }
    // 檢查大小 (HTML5 File API)
    if (fileInput.files[0].size > 5 * 1024 * 1024) { // 5MB
        alert('檔案不得超過 5MB');
        return false;
    }
    return true; // 允許提交
};
</script>

後端驗證:安全核心防線 (必須嚴格執行)

1. 基礎檢查:是否上傳、錯誤碼、大小

<?php
// 判斷是否有文件上傳 (關鍵:檢查 tmp_name 或 error 碼)
if (!isset($_FILES['file']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
    die('請選擇上傳檔案');
}

$file = $_FILES['file'];

// 錯誤碼檢查 (UPLOAD_ERR_OK === 0)
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 擴展阻止',
    ];
    die($errors[$file['error']] ?? '未知上傳錯誤');
}

// 大小限制 (雙重保險)
$maxSize = 5 * 1024 * 1024; // 5MB
if ($file['size'] > $maxSize) {
    die('檔案過大');
}
?>

2. 真實檔案類型偵測 (防偽造副檔名/MIME)

不可信 `$_FILES['file']['type']` (瀏覽器根據副檔名猜測,極易偽造)。

方法 A:`finfo` (Fileinfo 擴展,推薦、標準)

<?php
$allowedMimes = [
    'image/jpeg', 'image/png', 'image/gif', 'image/webp',
    'application/pdf',
];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']); // 真實 MIME

if (!in_array($mime, $allowedMimes, true)) {
    die('檔案類型不允許: ' . htmlspecialchars($mime));
}

// 可選:副檔名與 MIME 一致性檢查
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$mimeToExt = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/gif'  => 'gif',
    'image/webp' => 'webp',
    'application/pdf' => 'pdf',
];
if (($mimeToExt[$mime] ?? '') !== $ext) {
    die('副檔名與檔案內容不符');
}
?>

方法 B:魔術位元組 (Magic Bytes) 手動檢測 (無 fileinfo 時)

<?php
function detectMimeByMagic(string $path): string {
    $fp = fopen($path, 'rb');
    $header = fread($fp, 16); // 讀前 16 字節足夠大多數格式
    fclose($fp);
    
    $signatures = [
        // [偏移 => [十六進制特徵 => MIME]]
        0 => [
            'FFD8FF' => 'image/jpeg',
            '89504E470D0A1A0A' => 'image/png',
            '474946383761' => 'image/gif', // GIF87a
            '474946383961' => 'image/gif', // GIF89a
            '52494646' => 'image/webp',    // RIFF....WEBP (需進一步確認)
            '25504446' => 'application/pdf', // %PDF
        ],
        8 => [ // WebP 在偏移 8 處有 WEBP
            '57454250' => 'image/webp',
        ],
    ];
    
    $hex = bin2hex($header);
    foreach ($signatures as $offset => $patterns) {
        $sub = substr($hex, $offset * 2); // 十六進制字串偏移
        foreach ($patterns as $sig => $mime) {
            if (str_starts_with($sub, $sig)) return $mime;
        }
    }
    return 'application/octet-stream';
}

// 使用
$mime = detectMimeByMagic($file['tmp_name']);
if (!in_array($mime, $allowedMimes)) die('類型不允許');
?>

3. 安全存儲與命名

<?php
// 1. 生成安全文件名 (防覆蓋、防路徑遍歷、防執行)
$ext = $mimeToExt[$mime] ?? 'bin';
$safeName = bin2hex(random_bytes(16)) . '.' . $ext; // 32位隨機十六進制

// 2. 存儲目錄 (非 Web 可訪問目錄最佳,或配置 Web 服務器禁止腳本執行)
$uploadDir = __DIR__ . '/../storage/uploads/'; // 絕對路徑
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);

$destPath = $uploadDir . $safeName;

// 3. 移動文件 (is_uploaded_file 已驗證來源)
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
    die('文件保存失敗');
}

// 4. 設置權限 (去除執行權限)
chmod($destPath, 0644);

// 5. 記錄資料庫
// INSERT INTO uploads (original_name, stored_name, mime, size, user_id) VALUES ...
echo "上傳成功: {$safeName}";
?>

Web 服務器層面防護 (關鍵)

  • Nginx:上傳目錄 `location /uploads/ { add_header Content-Disposition attachment; location ~ \.php$ { deny all; } }`
  • Apache:上傳目錄 `.htaccess`:`SetHandler default-handler` + `Options -ExecCGI` + `php_flag engine off`。
  • 禁用腳本執行:上傳目錄**絕不**允許 PHP/Perl/Python 等腳本解析執行。

完整上傳處理流程圖

用戶提交表單
    ↓
前端 JS 檢查 (體驗優化,可繞過)
    ↓
後端 PHP:
  1. $_FILES 存在性 + is_uploaded_file()
  2. error 碼檢查
  3. 大小限制
  4. 【核心】finfo/magic bytes 真實 MIME 偵測
  5. 副檔名一致性檢查
  6. 生成安全隨機文件名
  7. move_uploaded_file() 到非執行目錄
  8. chmod 0644
  9. 入庫記錄
    ↓
返回結果
copyright © 萬盛學電腦網 all rights reserved