不能信$_files'file',因其由浏览器伪造、非服务端真实探测;必须用finfo_file()校验文件头、白名单限制扩展名、上传目录禁执行、强制重命名并分离存储路径。

直接靠 $_FILES['file']['type'] 做校验等于没校验,攻击者改个请求头就能绕过。真正的安全校验必须结合文件内容探测、扩展名白名单、MIME类型二次验证,并且上传目录本身不能被 Web 服务器直接执行。
为什么不能信 $_FILES['file']['type']
这个值完全由浏览器提交的 HTTP 请求头决定,不是 PHP 读取文件内容后得出的。用户用 curl 或 Burp Suite 伪造一个 Content-Type: image/jpeg,哪怕上传的是 shell.php,$_FILES['file']['type'] 也会显示 image/jpeg。
- 它只是客户端建议,服务端无义务遵守
- PHP 不会自动解析文件二进制头来验证该值是否真实
- 所有仅依赖此项的判断逻辑都可被轻易绕过
用 finfo_file() 检查真实 MIME 类型
这是 PHP 官方推荐的内容探测方式,基于 libmagic 库读取文件前几百字节,识别实际编码格式。它不依赖扩展名或请求头,更接近“文件是什么”的事实。
- 必须用
FILEINFO_MIME_TYPE模式,避免返回带参数的完整 MIME(如image/jpeg; charset=binary) - 只接受白名单中的类型,例如
['image/jpeg', 'image/png', 'application/pdf'],不要用strpos($mime, 'image/') !== false这类模糊匹配 - 调用前要确认
tmp_name是合法临时路径,且文件未被移动或删除
示例:
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$real_mime = finfo_file($finfo, $_FILES['upload']['tmp_name']);
finfo_close($finfo);
if (!in_array($real_mime, ['image/jpeg', 'image/png'])) {
die('Invalid file type');
}
上传目录必须禁用脚本执行能力
即使校验通过,如果上传目录允许执行 PHP,攻击者仍可能上传伪装成图片的 Webshell(比如在 JPEG 文件末尾追加 <?php system($_GET['cmd']); ?>),再通过 Apache 的默认配置触发解析。
- 在上传目录(如
uploads/)下放一个.htaccess,写入:SetHandler default-handler和php_flag engine off(Apache 2.4+ 推荐用<filesmatch> Require all denied </filesmatch>) - 确保该目录不在
DocumentRoot下,或至少不与可执行脚本同级;理想情况是放在 Web 根目录之外 - Linux 下设置目录权限为
755,文件为644,禁止777或g+w
重命名文件 + 分离存储路径是硬性要求
保留原始文件名风险极高:一是可能含路径遍历(../../etc/passwd),二是扩展名不可信(avatar.jpg.php 在某些 Apache 配置下仍会被当作 PHP 执行)。
- 生成唯一文件名,如
sha256_filecontent . '_' . time() . '.jpg',或用bin2hex(random_bytes(16)) - 从原始
name中提取扩展名时,只取最后一个.后的部分,并强制映射到白名单中已知安全的类型(如jpeg→jpg) - 存储路径应与 Web 可访问路径分离:文件物理存于
/var/data/uploads/,但通过 PHP 代理脚本(如serve.php?file=abc123.jpg)输出,配合.htaccess Deny from All禁止直连
最常被忽略的一点:校验和重命名必须在 move_uploaded_file() 之前完成。一旦文件落地,再检查就晚了——恶意内容已经写入磁盘,后续任何“删除”动作都可能被竞态条件绕过。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











