PHP 文件上传

The $_FILES structure, move_uploaded_file, type and size checks, and securing the upload directory.

Intermediate
Key PointsThe original name and MIME come from the client and are untrusted.

The form

An upload form needs enctype="multipart/form-data", otherwise nothing is sent.

HTML
<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="avatar">
    <input type="submit" value="上传">
</form>

$_FILES structure

KeyMeaning
name客户端原始文件名(可伪造)
type客户端声明的 MIME(可伪造)
tmp_name服务器上的临时文件路径
error错误码,0 表示成功
size字节数
PHP
<?php
// 假设上传了一个 20480 字节的 photo.png
$f = $_FILES["avatar"] ?? null;

if ($f === null) {
    exit("没有文件");
}

echo "原始名: " . $f["name"], "\n";
echo "大小: " . $f["size"] . " 字节\n";
echo "客户端声明类型: " . $f["type"], "\n";
echo "错误码: " . $f["error"], "\n";
echo "扩展名: " . strtolower(pathinfo($f["name"], PATHINFO_EXTENSION)), "\n";
Output
原始名: photo.png
大小: 20480 字节
客户端声明类型: image/png
错误码: 0
扩展名: png
type is guessed by the browser from the extension and is untrustworthy. Rename the file and it says image/png. Detect the real type with finfo_file() or getimagesize().

Error codes

CodeConstantMeaning
0UPLOAD_ERR_OK成功
1UPLOAD_ERR_INI_SIZE超过 upload_max_filesize
2UPLOAD_ERR_FORM_SIZE超过表单 MAX_FILE_SIZE
3UPLOAD_ERR_PARTIAL只上传了一部分
4UPLOAD_ERR_NO_FILE没有选择文件
6UPLOAD_ERR_NO_TMP_DIR缺临时目录
7UPLOAD_ERR_CANT_WRITE写磁盘失败
8UPLOAD_ERR_EXTENSION被扩展中断

The full handler

PHP
<?php
declare(strict_types=1);

function handleUpload(array $file, string $dir): array
{
    if (($file["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return ["ok" => false, "msg" => "上传失败,错误码 " . $file["error"]];
    }

    if ($file["size"] > 2 * 1024 * 1024) {
        return ["ok" => false, "msg" => "文件不能超过 2MB"];
    }

    $allowed = ["image/jpeg", "image/png", "image/gif"];
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file["tmp_name"]);
    if (!in_array($mime, $allowed, true)) {
        return ["ok" => false, "msg" => "只允许 JPG/PNG/GIF,实际是 $mime"];
    }

    if (@getimagesize($file["tmp_name"]) === false) {
        return ["ok" => false, "msg" => "不是有效图片"];
    }

    $ext = ["image/jpeg" => "jpg", "image/png" => "png", "image/gif" => "gif"][$mime];
    $name = bin2hex(random_bytes(8)) . "." . $ext;

    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    if (!move_uploaded_file($file["tmp_name"], $dir . "/" . $name)) {
        return ["ok" => false, "msg" => "保存失败"];
    }

    return ["ok" => true, "name" => $name, "mime" => $mime, "size" => $file["size"]];
}

$r = handleUpload(
    ["name" => "photo.png", "type" => "image/png", "tmp_name" => "/tmp/phpXXXX", "error" => 0, "size" => 20480],
    "/var/www/uploads"
);
print_r($r);
Output
Array
(
    [ok] => 1
    [name] => 3f2a9c1b7d4e5a60.png
    [mime] => image/png
    [size] => 20480
)

Regenerating the name with random_bytes discards the user's input entirely — killing traversal, encoding issues and collisions at once.

Security checklist

  • **Never trust $file["type"]** —— use finfo_file or getimagesize
  • Rename the file —— keeping the original name hands over path control
  • Disable PHP in the upload dir —— deny all for .php
  • Cap size and count —— ini settings
  • Store outside the webroot —— serve through PHP for authorisation
"PHP executable in the upload directory" is historically the worst hole. Even with strict image checks, one bypass yields a shell. This matters more than every other check.

Multiple files

PHP
<?php
// <input type="file" name="pics[]" multiple>
$files = $_FILES["pics"] ?? null;
if ($files === null) {
    exit("没有文件");
}

// 把「按字段组织」重排成「按文件组织」
$list = [];
foreach ($files["name"] as $i => $name) {
    $list[] = [
        "name" => $name,
        "type" => $files["type"][$i],
        "tmp_name" => $files["tmp_name"][$i],
        "error" => $files["error"][$i],
        "size" => $files["size"][$i],
    ];
}

echo count($list) . " 个文件\n";
foreach ($list as $f) {
    echo $f["name"] . " " . $f["size"] . "\n";
}
Output
2 个文件
a.png 10240
b.jpg 30720

For multiple files $_FILES is field-first: name is an array, type too. Reshape to file-first before looping.

Friends & Partners