PHP 文件上传排查,别一上来就怀疑代码写错
文件上传失败,很多时候不是业务代码的问题,而是请求入口、大小限制、临时文件或落盘路径其中一环没接住。
浏览器表单 → multipart 请求 → PHP 临时文件 → 校验文件 → move_uploaded_file() 落盘
我遇到最多的是头像上传:本地能传,线上传不上;小图能传,大图报错;看起来是图片,结果混进了脚本文件。处理这类问题,不要只盯着 move_uploaded_file(),要从 $_files 进入服务端那一刻开始看。
一、$_FILES 为空,先查 enctype 和字段名

后端拿不到文件时,先别急着改 PHP。页面表单如果没有 enctype="multipart/form-data",浏览器不会按文件上传的方式提交,服务端的 $_FILES 就可能是空的。
• 表单字段名是 avatar,后端就读 $_FILES['avatar']。
• 调试时先打印 $_FILES['avatar']['error'],别只看 tmp_name。
• 前端用异步上传时,确认 FormData 里真的追加了文件对象。
入口没进来,后面的校验写得再细也没用。
这个坑很常见,因为页面上选择文件的动作是正常的,开发者容易误以为请求也正常。看服务端数据,判断会更快。
二、UPLOAD_ERR_INI_SIZE 和 post_max_size 要一起看

头像稍微大一点就失败,问题通常在限制链路上。Nginx 可能先用 client_max_body_size 拦掉,PHP 也可能因为 upload_max_filesize 或 post_max_size 拿不到完整请求。
upload_max_filesize = 8M post_max_size = 10M max_file_uploads = 20
• 浏览器看到 413 Request Entity Too Large,先查 Nginx 入口限制。
• $_FILES['avatar']['error'] 是 UPLOAD_ERR_INI_SIZE,看 PHP 配置。
• post_max_size 要大于单文件大小,还要留出表单字段和请求开销。
大小限制不是一个开关,而是一条链。
线上排查时建议把错误码打进日志。只给用户返回“上传失败”,开发侧很难知道请求死在哪一层。
三、只看后缀不够,finfo_file() 和 getimagesize() 都要用

上传图片最危险的写法,是只判断文件名后缀。攻击者可以把脚本文件改成图片名,或者让文件内容和扩展名对不上。更靠谱的做法是同时看 MIME、图片尺寸和扩展名白名单。
$finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($_FILES['avatar']['tmp_name']); $size = getimagesize($_FILES['avatar']['tmp_name']);
• MIME 用 finfo_file() 或 finfo::file() 判断,别信浏览器传来的 type。
• 图片上传可以用 getimagesize() 兜一层,尺寸异常就拒绝。
• 白名单只放 jpg、jpeg、png、webp 这类明确格式。
文件名只能参考,不能当证据。
安全一点的上传目录也不要允许 PHP 执行。就算校验被绕过,目录权限也能再拦一层。
四、move_uploaded_file() 前后都要处理路径和文件名

文件校验通过后,落盘也容易出问题。move_uploaded_file() 只负责把临时文件移动到目标路径,不会帮你创建目录、处理重名、修权限,也不会替你决定文件名是否安全。
$name = bin2hex(random_bytes(16)) . '.jpg'; $target = __DIR__ . '/uploads/avatar/' . $name; move_uploaded_file($_FILES['avatar']['tmp_name'], $target);
• 用 is_uploaded_file() 确认来源,避免普通路径被当成上传文件。
• 不要直接使用用户原始文件名,容易覆盖文件,也容易带进奇怪字符。
• 目标目录要提前创建,权限只给写入需要的范围。
上传成功不代表存得安全。
头像上传这种功能看起来小,坑点却很密:入口格式、大小限制、文件内容、目录权限全都要过一遍。把错误码和落盘路径记清楚,排查速度会快很多。
别把上传当成一个函数调用,它其实是一条请求链路。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











