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

PHP is_uploaded_file() 與 move_uploaded_file() 安全實戰指南

     編輯:PHP 相關知識

為什麼需要專門的上傳驗證函數?

早期 PHP 直接用 copy($_FILES['file']['tmp_name'], $dest),攻擊者可偽造 $_FILES 數組,將 tmp_name 指向 /etc/passwd 等敏感文件,導致任意文件讀取。PHP 4.0.3+ 引入 is_uploaded_file() 和 move_uploaded_file(),通過內部哈希表 rfc1867_uploaded_files 驗證文件是否真由當前請求上傳,從根源阻斷偽造。

核心原理

// 簡化版內核邏輯
function is_uploaded_file(string $path): bool {
    global $rfc1867_uploaded_files; // 哈希表:key=臨時文件路徑, value=上傳元數據
    return isset($rfc1867_uploaded_files[$path]);
}

function move_uploaded_file(string $from, string $to): bool {
    if (!is_uploaded_file($from)) return false;
    return @rename($from, $to) || copy_and_unlink($from, $to);
}

標準安全上傳流程

<?php
function handleUpload(array $file, string $targetDir, array $allowedMimes = []): array {
    // 1. 錯誤碼檢查
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new Exception(uploadErrorMessage($file['error']));
    }

    // 2. 必須驗證是否真實上傳
    if (!is_uploaded_file($file['tmp_name'])) {
        throw new Exception('非法上傳文件');
    }

    // 3. MIME 類型二次驗證(不要信任 $_FILES['type'])
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->file($file['tmp_name']);
    if ($allowedMimes && !in_array($mime, $allowedMimes, true)) {
        throw new Exception("禁止的文件類型: $mime");
    }

    // 4. 文件大小(已由 PHP 限制,但可再次確認)
    if ($file['size'] > 10 * 1024 * 1024) { // 10MB
        throw new Exception('文件過大');
    }

    // 5. 生成安全文件名(防止路徑遍歷、覆蓋)
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $safeName = bin2hex(random_bytes(16)) . ($ext ? '.' . $ext : '');
    $targetPath = $targetDir . DIRECTORY_SEPARATOR . $safeName;

    // 6. 移動(原子操作,跨分區自動回退 copy+unlink)
    if (!move_uploaded_file($file['tmp_name'], $targetPath)) {
        throw new Exception('文件保存失敗');
    }

    // 7. 設置權限(去除執行位)
    chmod($targetPath, 0644);

    return ['path' => $targetPath, 'name' => $safeName, 'mime' => $mime, 'size' => $file['size']];
}

Windows 臨時文件名可預測的歷史漏洞(已修復)

早期 Windows PHP 臨時文件名為 C:\Windows\Temp\phpXXXX.tmp(XXXX 為十六進制遞增),攻擊者可通過錯誤回顯洩露當前文件名,再構造請求覆蓋 $_FILES 繞過驗證。現代 PHP(5.4+)已改用隨機前綴 php{random}.tmp,並加強 rfc1867_uploaded_files 隔離,此攻擊面基本關閉。

常見繞過嘗試與防禦

攻擊手法原理防禦
偽造 $_FILES 覆蓋extract($_POST) 導致變量覆蓋禁用 extract,顯式從 $_FILES 取值
利用 include 錯誤洩露 tmp_name觸發警告輸出路徑生產環境 display_errors=Off,log_errors=On
上傳 .php/.phtml 並通過解析漏洞執行Nginx/Apache 配置不當上傳目錄禁用腳本執行,文件重命名、去除執行權限
利用 move_uploaded_file TOCTOU極罕見,需精確競爭隨機文件名、目錄不可寫入腳本

進階:流式處理大文件(避免內存峰值)

<?php
// 直接將上傳流寫入目標,不經歷 tmp_name 磁盤落地(需 SAPI 支持)
// PHP 8.1+ 可用 fopen('php://input') 配合流過濾器
// 但大多數場景 move_uploaded_file 已足夠高效(內核 rename 原子操作)

最佳實踐清單

  1. 永遠使用 is_uploaded_file() 驗證,再用 move_uploaded_file() 移動。
  2. 文件名由服務端生成(隨機+擴展名白名單),絕不使用用戶原始文件名。
  3. 上傳目錄配置 php_admin_flag engine off (Apache) 或 location ~ \.php$ { deny all; } (Nginx)。
  4. MIME 類型用 finfo 二次驗證,擴展名僅作參考。
  5. 設置合理的 upload_max_filesize、post_max_size、max_file_uploads。
  6. 病毒掃描(ClamAV)集成到上傳流程(異步隊列)。
  7. 記錄審計日誌:用戶、IP、文件名、大小、MIME、存儲路徑、時間。

上傳功能是 Web 應用最高風險入口之一,嚴守「驗證-重命名-隔離執行」三部曲,方能安枕無憂。

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