首页 >  文章 >  php教程

PHP 文件上传 MIME 怎么验:finfo_file、临时文件与扩展名边界

来源:17golang原创

时间:2026-08-16 17:01:36 117浏览 收藏

开发上传接口时很容易碰到假安全的情况:前端做了 .jpg 限制,请求里携带的 MIME 字段也标注成 image/jpeg,服务端不做二次校验就直接把临时文件移动到公开目录里。实际校验环节最该优先信任的是PHP上传生成的临时文件本身的内容特征,同时还要把上传错误判断、大小校验、保存路径规则和访问方式一起收紧。

服务端不要单靠前端传来的扩展名、请求携带的 MIME 字段做文件类型判断,优先读取临时文件的内容特征做校验,再搭配白名单、大小限制、服务端生成文件名的规则,才能把文件上传的边界兜住。
要点速览
  • 扩展名和 $_FILES['file']['type'] 都来自请求侧,不能单独作为真实类型判断的依据。
  • finfo_file() 读取临时文件的内容特征,类型判断要和允许列表、大小上限一起完成校验。
  • 移动文件前先处理 UPLOAD_ERR_*、临时路径和文件名,保存时生成服务端专属名称,不要直接复用用户上传的原始文件名。
  • 通过 MIME 检查不等于文件可以直接公开访问;上传目录配置、响应头设置和二次处理规则仍要单独做好边界限制。

为什么只看扩展名和浏览器 MIME 不够

浏览器提交的原始文件名和 MIME 字段只适合用来做界面展示,完全不能承担安全判断的作用。攻击者可以把脚本文件强行改成 photo.jpg 后缀,也可以自行构造请求,把 type 字段写成看似完全正常的图片类型,PHP 官方上传文档也明确把这些字段标记为需要后续核验的不可信输入。

服务端首先要确认整个上传流程本身执行成功,再从 tmp_name 指向的临时文件读取类型特征。这个执行顺序很关键:文件上传失败时,临时路径可能为空或者根本没有可读内容,提前调用 Fileinfo 只会把真实的上传错误直接掩盖过去。

PHP 文件上传校验链:上传错误码、临时文件、finfo_file MIME 判断后进入拒绝或接收分支

用 finfo_file() 判断临时文件的真实类型

Fileinfo 扩展根据文件头部的 magic bytes 等内容特征推断文件类型。实际开发中通常使用 FILEINFO_MIME_TYPE,让返回结果保持为 image/jpegimage/png 这类标准 MIME 格式,再和服务端预设的允许列表做比对。

function detectUploadMime(string $tmpPath): string
{
    $info = new finfo(FILEINFO_MIME_TYPE);
    $mime = $info->file($tmpPath);

    if (!is_string($mime) || $mime === '') {
        throw new RuntimeException('cannot detect uploaded file type');
    }

    return strtolower(trim($mime));
}

$allowed = [
    'image/jpeg' => 'jpg',
    'image/png'  => 'png',
    'image/webp' => 'webp',
];

这个返回结果并不是“文件绝对安全”的证明,只是上传类型校验链路里的一层核心证据。Fileinfo 的识别能力也依赖运行环境里的 Fileinfo/libmagic 数据库,部署服务镜像之前要先确认扩展已经正确安装,用几类业务常用的真实样本验证返回值是否和项目预设的允许列表匹配。

把错误码、大小和 MIME 放进同一个校验入口

文件上传的错误码要最先做判断。UPLOAD_ERR_INI_SIZEUPLOAD_ERR_FORM_SIZE 对应大小超限的场景,UPLOAD_ERR_NO_FILE 代表用户没有选中任何文件就提交了表单;这些异常情况都不应该继续往下走保存逻辑。

function validateImageUpload(array $file, array $allowed, int $maxBytes): string
{
    $error = $file['error'] ?? UPLOAD_ERR_NO_FILE;
    if ($error !== UPLOAD_ERR_OK) {
        throw new InvalidArgumentException('upload failed: ' . $error);
    }

    $tmpPath = $file['tmp_name'] ?? '';
    if (!is_string($tmpPath) || $tmpPath === '' || !is_uploaded_file($tmpPath)) {
        throw new InvalidArgumentException('invalid upload temp file');
    }

    $size = filesize($tmpPath);
    if ($size === false || $size  $maxBytes) {
        throw new InvalidArgumentException('file size is outside the limit');
    }

    $mime = detectUploadMime($tmpPath);
    if (!array_key_exists($mime, $allowed)) {
        throw new InvalidArgumentException('file type is not allowed');
    }

    return $mime;
}

is_uploaded_file() 校验的是当前临时路径对应的文件确实来自 HTTP POST 上传机制。如果后续代码还要先读取、解析或者删除临时文件,调用这个方法能把校验边界卡得更清晰;就算不主动调用,后续准备调用 move_uploaded_file() 时,PHP 底层也会再次自动校验文件来源是否合法。

检查对象能回答的问题不能替代的检查
原始文件名用户期望展示的文件名称不能证明文件的真实类型
浏览器 MIME请求端自己声明的文件类型不能证明文件的实际内容
finfo_file()文件内容特征更接近哪一类 MIME不能直接决定文件是否可以公开访问
error 与 size上传流程是否成功、文件大小是否超限不能替代文件类型判断

保存时不要沿用用户上传的原始文件名

所有校验都通过之后,仍然不要把 $_FILES['file']['name'] 直接拼接到目标存储路径里。用户上传的文件名可能包含路径分隔符、特殊字符,还可能存在同名文件覆盖的风险。更稳妥的方案是由服务端生成随机名称,最终使用的后缀从已经校验通过的 MIME 类型映射得到。

$mime = validateImageUpload(
    $_FILES['file'] ?? [],
    $allowed,
    5 * 1024 * 1024,
);

$suffix = $allowed[$mime];
$storedName = bin2hex(random_bytes(16)) . '.' . $suffix;
$target = __DIR__ . '/../var/uploads/' . $storedName;

if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
    throw new RuntimeException('cannot store uploaded file');
}

chmod($target, 0640);

存放上传文件的目标文件夹最好放在 Web 根目录之外,由专属的下载接口按资源ID读取返回;如果业务要求文件必须放在公开路径下,也要避免服务器把未知后缀的文件当成脚本直接解析,给下载响应设置明确的 Content-TypeContent-Dispositionmove_uploaded_file() 执行成功时如果目标路径已经存在同名文件会直接覆盖,所以随机生成文件名不是无关紧要的装饰,而是避免同名文件误覆盖的一道实际保护规则。

PHP 上传存储边界:MIME 通过后生成服务端随机文件名,移入非公开目录并由下载层控制响应

图片类型校验通过后,还要不要做二次处理

如果业务只用来保存图片,建议在隔离的图片处理流程中重新编码,把编码输出的新文件作为最终存储对象使用。这样可以丢弃原始文件里所有不需要的附加内容,统一图片的尺寸、颜色和压缩策略。不要把“图片能被浏览器正常打开”误认为“可以直接交给任意解析库处理”;大型图片、异常尺寸和图片处理库的资源上限也要纳入校验范围。

针对头像、封面这类有固定尺寸要求的业务,可以在服务端额外读取图片宽高,拒绝极端宽高比和像素总数超限的图片。针对文档、压缩包和视频类文件,不要直接照搬图片的 MIME 白名单,要为每种格式设计单独的解析、病毒扫描或者人工审核流程。

上线前按失败路径做一次验收

  1. 提交空文件、超过 PHP 配置上限的文件和超过业务自定义上限的文件,确认返回的错误提示都符合预期。
  2. 把文本文件强行改成 .jpg 后缀提交,确认这类篡改后的文件没法通过 MIME 白名单校验。
  3. 上传真实的 JPEG、PNG、WebP 格式文件,确认 Fileinfo 返回结果、服务端生成的后缀和下载响应都完全匹配。
  4. 提交同名文件和带特殊字符的原始文件名,确认最终保存的文件名完全不受用户输入控制,也不会覆盖已有的旧文件。
  5. 检查上传文件夹的权限配置、Web 服务器的解析规则和运行日志,确认保存目录不会直接暴露未经处理的原始文件。

相关问题

$_FILES['type'] 能不能直接拿来判断图片?

不建议。它完全来自客户端请求,最多只能当成辅助参考信息;文件真实类型一定要读取服务端的临时文件,再和服务端预设的白名单比对才能确认。

finfo_file() 返回 image/jpeg 就一定安全吗?

不是。它只提供类型判断的核心证据,后续仍然要做好大小限制、像素校验、保存位置管控和后续处理流程的约束。

is_uploaded_file() 和 move_uploaded_file() 要同时调用吗?

如果后续操作只是移动文件,后者本身已经会自动检查文件是否来自合法上传;如果需要在移动之前先读取或者处理临时文件,主动调用前者能让来源校验的逻辑更清晰。

上传文件夹放在 public 目录下可以吗?

可以但风险会更高。优先把上传文件放在 Web 根目录外;如果必须放在公开访问的目录下,就要额外做好服务端生成名称、限制可解析后缀、明确下载响应规则的配套配置。

把上传校验当成一条完整的链路,而不是单个函数

PHP 文件上传的安全边界从来不在某一个 API 上:错误码判断保证上传流程本身合法,Fileinfo 提供内容类型的可信证据,大小和像素限制控制资源消耗,随机文件名完全隔离用户输入的影响,保存目录配置和下载层规则决定最终的暴露面。把这些校验逻辑放在同一条可测试的链路里,后续替换 Nginx、PHP-FPM 或者图片处理库时,才不会因为某一个字段看起来正常就放松整个上传入口的校验强度。

核对资料

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>