早期 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 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 原子操作)
is_uploaded_file() 驗證,再用 move_uploaded_file() 移動。php_admin_flag engine off (Apache) 或 location ~ \.php$ { deny all; } (Nginx)。finfo 二次驗證,擴展名僅作參考。upload_max_filesize、post_max_size、max_file_uploads。上傳功能是 Web 應用最高風險入口之一,嚴守「驗證-重命名-隔離執行」三部曲,方能安枕無憂。