文件上传是Web应用中非常常见的功能——用户头像、文章配图、附件上传、相册等等,都需要文件上传。但文件上传也是Web应用中最容易被攻击的地方之一。
如果文件上传处理不当,攻击者可以上传恶意文件——比如PHP脚本(webshell)、病毒、恶意HTML/JS文件——然后通过访问这些文件,执行恶意代码,获取服务器权限,窃取数据,篡改网页,甚至控制整个服务器。
很多网站被黑,都是因为文件上传漏洞。攻击者上传一个webshell,然后通过webshell控制服务器,为所欲为。文件上传漏洞,是OWASP十大安全风险之一,也是PHP网站最常见的安全漏洞之一。
今天分享PHP文件上传的安全防护措施,帮你写出安全的文件上传代码,防止恶意文件上传攻击。
文件上传的攻击方式
了解攻击者如何利用文件上传漏洞,才能更好地防护。
1. 直接上传脚本文件 最直接的攻击方式,就是上传PHP脚本文件(如shell.php),然后通过URL访问这个文件,执行PHP代码。如果上传目录在Web根目录下,且没有禁止执行PHP,攻击者就能直接执行上传的脚本。
2. 文件名绕过 攻击者通过各种方式绕过文件类型验证:
- 双扩展名:
shell.php.jpg(有些服务器会解析.php) - 特殊字符:
shell.php%00.jpg(空字节截断,老版本PHP) - 大小写:
shell.Php、shell.pHp(Windows不区分大小写) - 末尾加点或空格:
shell.php.、shell.php(Windows会自动去掉)
3. MIME类型伪造 很多程序只验证$_FILES['file']['type'](MIME类型),但这个值是客户端发送的,可以伪造。攻击者可以把PHP文件的MIME类型改成image/jpeg,绕过验证。
4. 图片马 攻击者把PHP代码嵌入到图片文件中(在图片末尾追加PHP代码),文件头是正常的图片,文件内容包含PHP代码。如果服务器包含(include)这个文件,或者配置了把图片当PHP解析,就能执行恶意代码。
5. .htaccess攻击 攻击者上传.htaccess文件,修改服务器配置,比如把jpg文件当PHP解析,然后上传图片马,就能执行恶意代码。
6. 解析漏洞 利用服务器的解析漏洞,比如:
- Apache:
shell.php.jpg可能被解析为PHP - IIS:
shell.asp;jpg、shell.asp/等 - Nginx:
shell.jpg%00.php、shell.jpg/.php等
安全防护措施
针对以上攻击方式,我们需要多层防护,确保文件上传安全。
1. 验证文件类型
不要只靠扩展名或MIME类型,要多种方式结合验证:
验证扩展名:
$allowedExts = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
die("不允许的文件类型");
}验证MIME类型(辅助):
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($_FILES['file']['type'], $allowedMimes)) {
die("不允许的文件类型");
}验证文件内容(最可靠): 用finfo或getimagesize验证文件实际内容:
// 用finfo验证MIME类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedMimes)) {
die("文件内容不合法");
}
// 图片文件用getimagesize验证
$imageInfo = @getimagesize($_FILES['file']['tmp_name']);
if ($imageInfo === false) {
die("不是有效的图片文件");
}2. 限制文件大小
限制上传文件的大小,避免大文件攻击和DoS攻击:
$maxSize = 2 * 1024 * 1024; // 2MB
if ($_FILES['file']['size'] > $maxSize) {
die("文件太大,最大允许2MB");
}同时在php.ini中配置:
upload_max_filesize = 2M
post_max_size = 8M3. 重命名文件
不要用原始文件名,重新生成随机文件名,避免文件名攻击和覆盖:
// 生成随机文件名
$newFilename = uniqid('img_', true) . '.' . $ext;
// 或者用更随机的方式
$newFilename = md5(uniqid(mt_rand(), true)) . '.' . $ext;不要用用户ID、时间戳等可预测的文件名,避免被猜到。
4. 安全存储位置
把上传文件存储在Web根目录之外: 这是最安全的方式,上传的文件不能通过URL直接访问,需要通过PHP脚本读取输出:
$uploadDir = '/var/www/uploads/'; // Web根目录之外
$savePath = $uploadDir . $newFilename;
move_uploaded_file($_FILES['file']['tmp_name'], $savePath);
// 访问时通过PHP脚本读取
// file.php?file=xxx.jpg
header("Content-Type: image/jpeg");
readfile($uploadDir . $_GET['file']);如果必须存在Web根目录下:
- 禁止上传目录执行PHP(在Nginx/Apache配置)
- 上传目录不要有执行权限
- 不要把上传目录作为include_path
5. 禁止执行权限
在服务器配置中,禁止上传目录执行脚本:
Nginx配置:
location ^~ /uploads/ {
location ~ \.php$ {
deny all; # 禁止访问php文件
}
}Apache配置(.htaccess):
<Files ~ "\.php$">
Order Allow,Deny
Deny from all
</Files>注意:不要允许用户上传.htaccess文件!
6. 图片处理(重新生成图片)
对于图片上传,用GD或ImageMagick重新处理图片,生成新的图片文件,可以清除图片中嵌入的恶意代码:
$src = imagecreatefromjpeg($_FILES['file']['tmp_name']);
$dst = imagecreatetruecolor($width, $height);
imagecopyresampled($dst, $src, 0, 0, 0, 0, $width, $height, $origWidth, $origHeight);
imagejpeg($dst, $savePath, 90);
imagedestroy($src);
imagedestroy($dst);重新生成的图片,只保留图片像素数据,清除了嵌入的PHP代码,非常安全。
7. 检查上传错误
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
die("文件太大");
case UPLOAD_ERR_PARTIAL:
die("文件只有部分被上传");
case UPLOAD_ERR_NO_FILE:
die("没有文件被上传");
default:
die("上传失败");
}
}8. 防XSS
如果要显示用户上传的文件名,一定要转义,防止XSS:
echo htmlspecialchars($_FILES['file']['name'], ENT_QUOTES, 'UTF-8');9. 用isuploadedfile和moveuploadedfile
if (is_uploaded_file($_FILES['file']['tmp_name'])) {
move_uploaded_file($_FILES['file']['tmp_name'], $savePath);
} else {
die("非法上传");
}不要用copy()或rename(),要用moveuploadedfile(),它会检查文件是否是合法的上传文件。
完整的安全上传示例
<?php
// 配置
$allowedExts = ['jpg', 'jpeg', 'png', 'gif'];
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
$maxSize = 2 * 1024 * 1024; // 2MB
$uploadDir = '/var/www/uploads/'; // Web根目录之外
// 检查上传错误
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
die("上传失败");
}
// 检查文件大小
if ($_FILES['file']['size'] > $maxSize) {
die("文件太大");
}
// 验证扩展名
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts)) {
die("不允许的文件类型");
}
// 验证MIME类型
if (!in_array($_FILES['file']['type'], $allowedMimes)) {
die("不允许的文件类型");
}
// 验证文件内容(finfo)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedMimes)) {
die("文件内容不合法");
}
// 验证图片(如果是图片)
$imageInfo = @getimagesize($_FILES['file']['tmp_name']);
if ($imageInfo === false) {
die("不是有效的图片");
}
// 生成随机文件名
$newFilename = md5(uniqid(mt_rand(), true)) . '.' . $ext;
$savePath = $uploadDir . $newFilename;
// 移动文件
if (move_uploaded_file($_FILES['file']['tmp_name'], $savePath)) {
// 可选:重新处理图片,清除恶意代码
// ...
echo "上传成功:" . $newFilename;
} else {
die("上传失败");
}其他注意事项
- 不要相信任何客户端验证:JavaScript验证只是为了用户体验,服务端一定要重新验证
- 不要显示上传路径:不要把上传文件的服务器路径暴露给用户
- 上传目录不要有执行权限:Linux下chmod 644文件,755目录,不要777
- 定期清理上传目录:删除不需要的文件,避免磁盘占满
- 病毒扫描:如果允许上传非图片文件(如doc、pdf、zip),建议用杀毒软件扫描
- CDN存储:可以把上传文件存到云存储(如阿里云OSS、腾讯云COS),更安全更省心
- HTTPS:上传用HTTPS,防止中间人攻击
- CSRF防护:上传表单加CSRF Token,防止CSRF攻击
总结
文件上传是Web应用中最容易被攻击的地方,一定要做好安全防护。
核心防护措施:
- 验证文件类型(扩展名+MIME+文件内容,多重验证)
- 限制文件大小
- 重命名文件(随机文件名)
- 存储在Web根目录之外(最安全)
- 禁止上传目录执行脚本
- 图片重新处理(清除恶意代码)
- 检查上传错误
- 用moveuploadedfile
- 防XSS(输出转义)
安全是一个系统工程,不要只靠一层防护,要多层防护,纵深防御。即使某一层被绕过,还有其他层保护。
希望这篇文章能帮你写出安全的文件上传代码,防止恶意文件上传攻击。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录