PHP is_uploaded_file()函数

is_uploaded_file() 函数用于检查指定的文件是否是通过 HTTP POST 上传的。这是验证上传文件安全性的关键函数,可以防止攻击者使用其他路径的文件冒充上传文件。

语法

bool is_uploaded_file ( string $filename )

参数说明

参数 描述
filename 必需。规定要检查的文件临时路径,通常是 $_FILES['file']['tmp_name'] 的值。

返回值

如果文件是通过 HTTP POST 上传的,返回 TRUE,否则返回 FALSE

注意:这个函数对于确保上传文件的安全性至关重要。应该总是使用这个函数来验证文件,然后再使用 move_uploaded_file() 移动文件。

示例1:基本用法

基本的文件上传验证:

<?php
// 检查是否有文件上传
if (isset($_FILES['userfile']) && $_FILES['userfile']['error'] === UPLOAD_ERR_OK) {
    $tmp_name = $_FILES['userfile']['tmp_name'];
    $original_name = $_FILES['userfile']['name'];

    echo "上传的文件名: $original_name<br>";
    echo "临时文件路径: $tmp_name<br><br>";

    // 验证是否是有效的上传文件
    if (is_uploaded_file($tmp_name)) {
        echo "✓ 文件是通过HTTP POST上传的,是有效的上传文件。<br>";

        // 现在可以安全地移动文件
        $target_path = "uploads/" . basename($original_name);

        if (move_uploaded_file($tmp_name, $target_path)) {
            echo "✓ 文件已成功移动到: $target_path";
        } else {
            echo "✗ 移动文件失败。";
        }
    } else {
        echo "✗ 文件不是通过HTTP POST上传的,可能存在安全问题!";
    }
} else {
    echo "没有文件上传或上传过程中出现错误。";
}
?>
对应的HTML表单:
<form action="upload.php" method="post" enctype="multipart/form-data">
    <div class="mb-3">
        <label for="userfile" class="form-label">选择文件:</label>
        <input type="file" class="form-control" id="userfile" name="userfile">
    </div>
    <button type="submit" class="btn btn-primary">上传文件</button>
</form>

示例2:多文件上传验证

验证多个上传文件:

<?php
if (isset($_FILES['files'])) {
    $files = $_FILES['files'];
    $upload_results = [];

    echo "文件上传处理报告:<br><br>";

    // 遍历所有上传的文件
    for ($i = 0; $i < count($files['name']); $i++) {
        $filename = $files['name'][$i];
        $tmp_name = $files['tmp_name'][$i];
        $error = $files['error'][$i];

        echo "<div class='mb-3 p-2 border rounded'>";
        echo "<strong>文件: $filename</strong><br>";

        if ($error === UPLOAD_ERR_OK) {
            // 验证是否是有效的上传文件
            if (is_uploaded_file($tmp_name)) {
                echo "✓ 验证通过(有效的上传文件)<br>";

                // 安全检查:文件类型和大小
                $allowed_types = ['image/jpeg', 'image/png', 'application/pdf'];
                $max_size = 2 * 1024 * 1024; // 2MB
                $file_size = $files['size'][$i];
                $file_type = $files['type'][$i];

                // 检查文件大小
                if ($file_size > $max_size) {
                    echo "✗ 文件过大 ($file_size 字节,最大允许 $max_size 字节)<br>";
                    continue;
                }

                // 检查文件类型
                if (!in_array($file_type, $allowed_types)) {
                    echo "✗ 不支持的文件类型: $file_type<br>";
                    continue;
                }

                // 生成安全文件名
                $safe_filename = time() . "_" . preg_replace("/[^a-zA-Z0-9\._-]/", "", $filename);
                $target_path = "uploads/" . $safe_filename;

                // 移动文件
                if (move_uploaded_file($tmp_name, $target_path)) {
                    echo "✓ 文件已保存为: $safe_filename<br>";
                    $upload_results[] = [
                        'original' => $filename,
                        'saved' => $safe_filename,
                        'path' => $target_path
                    ];
                } else {
                    echo "✗ 保存文件失败<br>";
                }
            } else {
                echo "✗ 安全验证失败:不是有效的上传文件!<br>";
            }
        } else {
            echo "✗ 上传错误代码: $error<br>";
            switch ($error) {
                case UPLOAD_ERR_INI_SIZE:
                    echo "文件大小超过服务器限制";
                    break;
                case UPLOAD_ERR_FORM_SIZE:
                    echo "文件大小超过表单限制";
                    break;
                case UPLOAD_ERR_PARTIAL:
                    echo "文件只有部分被上传";
                    break;
                case UPLOAD_ERR_NO_FILE:
                    echo "没有文件被上传";
                    break;
                case UPLOAD_ERR_NO_TMP_DIR:
                    echo "找不到临时文件夹";
                    break;
                case UPLOAD_ERR_CANT_WRITE:
                    echo "写入磁盘失败";
                    break;
                case UPLOAD_ERR_EXTENSION:
                    echo "PHP扩展停止了文件上传";
                    break;
            }
            echo "<br>";
        }

        echo "</div>";
    }

    // 显示上传结果摘要
    if (!empty($upload_results)) {
        echo "<h5>上传成功摘要:</h5>";
        echo "<ul>";
        foreach ($upload_results as $result) {
            echo "<li>{$result['original']} → {$result['saved']}</li>";
        }
        echo "</ul>";
    }
}
?>
对应的HTML表单:
<form action="multi_upload.php" method="post" enctype="multipart/form-data">
    <div class="mb-3">
        <label for="files" class="form-label">选择多个文件:</label>
        <input type="file" class="form-control" id="files" name="files[]" multiple>
        <div class="form-text">支持的文件类型: JPEG, PNG, PDF (最大2MB)</div>
    </div>
    <button type="submit" class="btn btn-primary">上传多个文件</button>
</form>

示例3:安全的文件上传处理类

创建一个安全的文件上传处理类:

<?php
class SecureFileUploader {
    private $allowed_types = [];
    private $max_size = 2097152; // 2MB
    private $upload_dir = 'uploads/';
    private $errors = [];

    public function __construct($upload_dir = 'uploads/') {
        $this->upload_dir = rtrim($upload_dir, '/') . '/';

        // 确保上传目录存在
        if (!is_dir($this->upload_dir)) {
            mkdir($this->upload_dir, 0755, true);
        }
    }

    public function setAllowedTypes($types) {
        $this->allowed_types = $types;
    }

    public function setMaxSize($size_in_bytes) {
        $this->max_size = $size_in_bytes;
    }

    public function upload($file_input_name) {
        if (!isset($_FILES[$file_input_name])) {
            $this->errors[] = "没有文件上传";
            return false;
        }

        $file = $_FILES[$file_input_name];

        // 检查上传错误
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = "上传错误代码: " . $file['error'];
            return false;
        }

        $tmp_path = $file['tmp_name'];
        $original_name = $file['name'];
        $file_size = $file['size'];
        $file_type = $file['type'];

        // 关键验证:检查是否是真正的上传文件
        if (!is_uploaded_file($tmp_path)) {
            $this->errors[] = "安全验证失败:不是有效的上传文件";
            return false;
        }

        // 验证文件大小
        if ($file_size > $this->max_size) {
            $this->errors[] = "文件过大 ({$file_size} 字节,最大允许 {$this->max_size} 字节)";
            return false;
        }

        // 验证文件类型
        if (!empty($this->allowed_types) && !in_array($file_type, $this->allowed_types)) {
            $this->errors[] = "不支持的文件类型: {$file_type}";
            return false;
        }

        // 生成安全文件名
        $extension = pathinfo($original_name, PATHINFO_EXTENSION);
        $safe_name = uniqid('upload_', true) . '.' . strtolower($extension);
        $target_path = $this->upload_dir . $safe_name;

        // 移动文件
        if (!move_uploaded_file($tmp_path, $target_path)) {
            $this->errors[] = "移动文件失败";
            return false;
        }

        // 返回上传文件信息
        return [
            'original_name' => $original_name,
            'saved_name' => $safe_name,
            'file_path' => $target_path,
            'file_size' => $file_size,
            'file_type' => $file_type,
            'upload_time' => time()
        ];
    }

    public function getErrors() {
        return $this->errors;
    }
}

// 使用示例
$uploader = new SecureFileUploader('uploads/');
$uploader->setAllowedTypes(['image/jpeg', 'image/png', 'application/pdf']);
$uploader->setMaxSize(5 * 1024 * 1024); // 5MB

$result = $uploader->upload('userfile');

if ($result) {
    echo "文件上传成功!<br><br>";
    echo "原始文件名: " . $result['original_name'] . "<br>";
    echo "保存文件名: " . $result['saved_name'] . "<br>";
    echo "文件大小: " . $result['file_size'] . " 字节<br>";
    echo "文件类型: " . $result['file_type'] . "<br>";
    echo "保存路径: " . $result['file_path'] . "<br>";
} else {
    echo "文件上传失败!<br><br>";
    echo "错误信息:<br>";
    foreach ($uploader->getErrors() as $error) {
        echo "- $error<br>";
    }
}
?>

示例4:安全威胁和防御

展示不使用 is_uploaded_file() 的安全风险:

<?php
// ❌ 危险:不使用 is_uploaded_file() 验证
function unsafe_file_upload($tmp_path, $target_path) {
    // 攻击者可以传递任何文件路径,例如 /etc/passwd
    if (file_exists($tmp_path)) {
        // 直接复制文件 - 这是不安全的!
        copy($tmp_path, $target_path);
        return true;
    }
    return false;
}

// ✅ 安全:使用 is_uploaded_file() 验证
function safe_file_upload($tmp_path, $target_path) {
    // 验证是否是真正的上传文件
    if (is_uploaded_file($tmp_path)) {
        // 使用 move_uploaded_file() 移动文件
        return move_uploaded_file($tmp_path, $target_path);
    }
    return false;
}

// 模拟攻击场景
echo "<h5>攻击场景模拟:</h5>";
echo "假设攻击者试图上传 /etc/passwd 文件<br><br>";

$malicious_tmp_path = "/etc/passwd";
$target = "uploads/passwd_copy.txt";

echo "攻击者提供的路径: $malicious_tmp_path<br>";

// 不安全的方式
echo "<br><strong>不安全的方式(不使用is_uploaded_file()):</strong><br>";
if (unsafe_file_upload($malicious_tmp_path, $target)) {
    echo "✗ 危险!系统文件被复制到: $target<br>";
    echo "攻击者成功访问了敏感文件!";
} else {
    echo "✓ 文件复制失败(可能是权限问题)";
}

// 安全的方式
echo "<br><br><strong>安全的方式(使用is_uploaded_file()):</strong><br>";
if (safe_file_upload($malicious_tmp_path, $target)) {
    echo "✗ 文件移动成功";
} else {
    echo "✓ 安全!is_uploaded_file() 检测到这不是有效的上传文件<br>";
    echo "攻击被阻止!";
}
?>
重要安全提示: 永远不要使用 copy()rename()file_put_contents() 来处理上传文件。必须使用 is_uploaded_file() 验证后,再用 move_uploaded_file() 移动文件。

重要注意事项

  1. 安全验证:is_uploaded_file() 是防止文件上传攻击的关键。它确保文件来自HTTP POST请求,而不是来自其他路径。
  2. 必须与 move_uploaded_file() 配对: 验证后,应该只使用 move_uploaded_file() 移动文件,因为它包含额外的安全检查。
  3. 临时文件: 上传的文件首先存储在临时目录中(由php.ini中的 upload_tmp_dir 指定)。脚本结束后,这些文件会被自动删除。
  4. 文件名安全: 即使文件通过了验证,也不应直接使用用户提供的文件名。应该生成新的安全文件名。
  5. 额外验证: 除了验证上传,还应检查文件类型、大小和内容,防止上传恶意文件。
  6. 性能考虑: 上传大文件时,确保服务器配置(upload_max_filesizepost_max_sizemax_execution_time)允许。

相关函数

move_uploaded_file()

将上传的文件移动到新位置

$_FILES 超全局变量

包含所有上传文件的信息

UPLOAD_ERR_XXX 常量

文件上传错误代码

pathinfo()

返回文件路径信息

basename()

返回路径中的文件名部分

getimagesize()

获取图像尺寸和类型信息

文件上传最佳实践

应该做的事
  • 总是使用 is_uploaded_file() 验证文件
  • 总是使用 move_uploaded_file() 移动文件
  • 验证文件类型(MIME类型和文件扩展名)
  • 限制文件大小
  • 生成安全的随机文件名
  • 将上传目录配置为不可执行
不应该做的事
  • 不要信任用户提供的文件名
  • 不要使用 copy()rename() 处理上传文件
  • 不要将上传文件存储在web根目录下
  • 不要直接执行上传的文件
  • 不要仅依赖客户端验证
  • 不要显示详细的错误信息给用户