PHP 图片上传有哪些安全问题?文件类型、大小和路径检查不能忽视
图片上传是 PHP 网站中非常常见的功能,用户头像、文章配图、商品图片、附件以及后台编辑器中的媒体文件,都可能涉及文件上传。代码层面看起来往往只是接收 $_FILES,检查一下扩展名,然后使用 move_uploaded_file() 保存文件,但从服务器安全角度看,上传功能实际上把一份完全由外部用户控制的数据交给了服务器处理。
这意味着风险并不只存在于“上传了一个 PHP 文件”这种最典型的场景。文件类型判断错误、文件名可控、目录权限过大、Web 服务器执行上传文件、图片解析器漏洞、超大图片消耗 CPU 和内存,以及没有限制上传频率,都可能成为攻击入口。安全的 PHP 图片上传应该采用多层验证,而不是依靠一个函数或者一条扩展名规则解决全部问题。
一、不要相信浏览器提供的 MIME 类型
PHP 的 $_FILES['file']['type'] 经常被初学者拿来判断文件是不是图片。例如:
if ($_FILES['file']['type'] === 'image/jpeg') {
// 允许上传
}
这种判断不能作为安全边界。
这个字段来自客户端提交的数据,攻击者可以自己构造 HTTP 请求,因此完全可以把一个并非 JPEG 的文件声明成 image/jpeg。
服务器应该自行检测上传文件,而不是相信浏览器告诉服务器“我上传的是图片”。
PHP 可以使用 Fileinfo 扩展中的 finfo_file() 对文件进行 MIME 类型检测。例如网站只允许 JPEG、PNG 和 WebP,那么程序就应该明确列出允许的 MIME 类型,并拒绝其他类型。
但 MIME 检测也不能被理解成绝对可靠的“真图片认证”。它应该和扩展名、图片解码、尺寸检查以及最终存储策略结合使用。
二、扩展名应该采用白名单
文件扩展名同样需要检查,但不要采用黑名单思维。
例如只检查:
if ($ext !== 'php') {
// 允许
}
这种设计的问题在于攻击者可能使用其他可执行扩展名,或者利用服务器配置与应用程序对文件类型理解不同所产生的漏洞。
如果业务只需要 JPEG、PNG 和 WebP,那么程序应该直接规定:
jpg
jpeg
png
webp
其他扩展名全部拒绝。
同时,最终保存的文件扩展名最好由服务器根据经过验证的文件类型决定,而不是直接沿用用户上传时提供的文件名。
三、文件名不能直接交给服务器
下面这种写法风险很高:
$target = '/var/www/uploads/' . $_FILES['file']['name'];
move_uploaded_file(
$_FILES['file']['tmp_name'],
$target
);
这里的最终文件名完全由客户端控制。
文件名可能包含特殊字符、路径片段,也可能造成文件覆盖。如果应用进一步允许用户提交目录名称,风险会扩大到路径遍历。
安全的做法是由服务器重新生成文件名。
例如可以使用 random_bytes() 生成随机标识符,然后由服务器确定扩展名:
$name = bin2hex(random_bytes(16)) . '.jpg';
用户原始文件名如果业务上需要保存,可以作为数据库中的显示信息记录,但不应该直接成为服务器实际存储路径。
四、路径遍历不能只靠过滤 ../
很多开发者看到路径遍历,就想到过滤:
../
这种方法并不可靠。
文件系统路径在不同操作系统、Web 服务器和应用程序中的处理方式存在差异。安全设计不应该建立在“我已经过滤了几个特殊字符”之上。
更合理的做法是让用户完全无法决定服务器的存储路径。
例如程序固定使用:
/storage/uploads/
然后由服务器按照日期、资源 ID 或随机 ID 组织目录。
用户只负责提供文件,服务器决定文件存在哪里。
五、上传目录最好放在 Web 根目录之外
如果图片没有必要通过 URL 直接访问,可以将上传文件存放在 Web root 之外。
例如网站可以设计成:
/home/site/public/
index.php
css/
js/
/home/site/storage/
uploads/
这样,即使上传目录中出现一个恶意文件,浏览器也无法通过普通 URL 直接访问它。
如果业务需要公开显示图片,则可以通过受控的静态文件路径、应用下载接口或者对象存储提供访问。
这种架构比把所有用户上传文件直接放在:
/public/uploads/
下面更容易控制风险。
六、如果上传目录必须公开访问,就禁止脚本执行
很多现有 PHP 网站已经把图片放在 Web 根目录下面,这种情况下最重要的一项措施就是确保上传目录不能执行 PHP 等服务器端脚本。
假设攻击者成功上传:
test.php
如果 Apache 将其交给 PHP-FPM 执行,那么一个看似普通的图片上传漏洞就可能变成服务器端代码执行漏洞。
因此,上传目录应该按照 Web 服务器的实际配置明确禁止 PHP、CGI 等脚本执行。
同时还应该防止攻击者上传或者影响 .htaccess、web.config 等服务器配置文件。
这里需要根据 Apache、Nginx、PHP-FPM 以及主机环境分别配置,不能简单认为“文件扩展名检查通过了”就安全。
七、getimagesize() 可以辅助检查,但不能单独判断文件安全
PHP 中的 getimagesize() 经常出现在图片上传教程里。
它可以读取图片的宽度、高度以及部分图片信息,因此在判断图片尺寸方面非常有用。
但它不应该被当成“这个文件绝对是一张安全图片”的验证函数。
如果代码只是:
if (getimagesize($tmp)) {
move_uploaded_file($tmp, $target);
}
就直接保存原始文件,安全性仍然不足。
更合理的做法是结合 Fileinfo、允许的扩展名、图片解码以及尺寸限制进行判断。
对于需要进一步处理的图片,还可以使用 GD 或 ImageMagick 等库实际解码图片,然后重新编码成网站允许的格式。
八、不要通过搜索恶意字符串来检查图片
有些教程建议使用:
file_get_contents()
把整个图片读取出来,然后搜索:
$maxSize) {
exit('File too large');
}
随后进行扩展名、MIME、图片解码和尺寸检查。
只有这些验证全部通过之后,才进入正式存储流程。
这种分层设计比把几十项检查全部塞进一个巨大的 if 条件中更容易维护,也更方便定位上传失败原因。
十六、上传目录权限应该遵循最小权限原则
服务器上经常可以看到:
chmod 777 uploads
这种处理方式。
它确实可能暂时解决“PHP 没有写权限”的问题,但代价是把目录权限放得过大。
更合理的方法是让实际运行 PHP-FPM 或 Web 服务的系统账户拥有完成业务所需的最低权限。
上传目录通常需要应用写入,但并不意味着系统中的所有用户都应该拥有写入、修改和执行权限。
也不要把 777 当成 PHP 上传问题的标准解决方案。
十七、数据库权限和文件权限是两个不同层面
如果网站使用数据库记录上传文件,那么数据库中可以保存:
文件 ID
所属用户
原始文件名
服务器生成的文件名
文件类型
文件大小
创建时间
存储状态
用户访问图片时,应用应该根据当前用户身份判断是否有权访问对应资源。
例如某个用户上传的私有文件,不能仅仅因为攻击者猜到了文件名,就可以直接获得访问权限。
随机文件名可以降低猜测概率,但不能代替访问控制。
十八、公开图片也应该考虑 Content-Type 和浏览器处理方式
对于普通 JPEG、PNG、WebP 图片,服务器应该正确返回对应的 Content-Type。
如果网站允许上传 SVG、HTML、PDF 等其他类型,则需要单独考虑主动内容、跨站脚本以及浏览器内容嗅探等问题。
如果业务根本不需要 SVG,就没有必要为了“支持更多格式”而开放 SVG。
上传系统支持的文件类型越多,需要处理的解析器和浏览器行为就越复杂。
十九、日志和异常处理不能省略
上传系统应该记录重要事件,例如谁上传了文件、上传了什么类型、验证是否通过、文件是否成功保存以及文件是否被删除。
发生异常时,还应该检查 Web 服务器、PHP-FPM 和应用程序日志。
常见问题包括:
PHP 上传限制
POST 请求大小限制
临时目录不可写
正式目录权限不足
磁盘空间不足
Fileinfo 不可用
图片解析失败
图片处理库异常
PHP-FPM 超时
生产环境不应该直接把 PHP 内部错误、服务器路径或者堆栈信息显示给用户。用户只需要得到明确的失败提示,详细错误应该进入服务器日志。
二十、还需要防止上传接口被大量调用
文件上传不仅有文件安全问题,还有资源消耗问题。
假设每张图片只允许 5 MB,但攻击者可以每秒提交几十次合法图片上传请求,那么即使每一个文件都通过类型验证,也可能迅速消耗磁盘、CPU、内存和数据库资源。
因此,对公开上传接口,还应该根据用户、IP、账户、接口和业务场景设置合理的频率限制。
对于后台管理员上传,也可以设置更宽松但仍然存在的请求保护。
二十一、图片上传安全应该采用纵深防御
一个成熟的 PHP 图片上传系统,可以按照下面的顺序设计:
检查用户身份。
检查用户是否具有上传权限。
检查 CSRF。
检查 $_FILES 错误状态。
检查文件大小。
检查扩展名白名单。
使用 Fileinfo 检测 MIME 类型。
根据业务检查图片尺寸。
使用图片处理库实际解码图片。
根据需要重新编码图片。
由服务器生成随机文件名。
使用服务器确定存储目录。
将文件保存到隔离的存储位置。
确保上传目录不能执行 PHP 等脚本。
设置最小化文件系统权限。
记录上传和失败事件。
对公开上传接口实施频率限制。
定期更新 PHP、Web 服务器和图片处理库。
这里没有哪一个步骤可以单独承担全部安全责任。
文件扩展名可以被伪造,MIME 可以被伪造,图片解析可能存在漏洞,随机文件名也不能解决访问控制问题,目录权限同样不能代替类型验证。安全性来自这些措施组合之后形成的防护层。
对于普通 PHP 网站,如果业务只是上传文章图片、头像和商品图片,没有必要把系统设计得过度复杂。优先做好允许格式白名单、服务端文件大小限制、Fileinfo 检测、图片解码和重编码、随机文件名、Web root 隔离以及禁止上传目录执行脚本这几个环节,就已经能够消除大量常见风险。
图片上传功能真正难的地方,不是写出一个可以把文件从临时目录移动到 uploads 目录的 PHP 函数,而是确保这份来自互联网的、不可信数据,在进入服务器文件系统、图片解析器、Web 服务器和数据库之后,每一个环节都不会获得超出业务需要的权限。对于生产环境来说,这种思路比不断增加几个扩展名黑名单或者扫描几个恶意字符串更加可靠。
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP