Appearance
16.3 防文件上传漏洞
文件上传漏洞原理
文件上传漏洞是指攻击者通过上传恶意文件到服务器,从而执行恶意代码、获取服务器权限或破坏网站的安全漏洞。这种漏洞通常出现在允许用户上传文件的功能中,如头像上传、附件上传等。
攻击示例
- 上传恶意脚本:攻击者上传包含恶意代码的 PHP、ASP、JSP 等文件,然后通过访问该文件执行恶意代码。
- 上传 Webshell:攻击者上传 Webshell(网站后门),获得服务器的控制权。
- 上传恶意文件:攻击者上传病毒、木马等恶意文件,危害服务器安全。
- 绕过文件类型检查:攻击者通过修改文件扩展名、MIME 类型等方式绕过服务器的文件类型检查。
防止文件上传漏洞的方法
1. 限制文件类型
通过白名单方式限制允许上传的文件类型,只允许上传安全的文件类型。
php
// 允许上传的文件类型白名单
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
// 检查文件类型
if (!in_array($_FILES['file']['type'], $allowed_types)) {
die("只允许上传 JPG、PNG、GIF 图片文件");
}
// 或者检查文件扩展名
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
$file_extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($file_extension, $allowed_extensions)) {
die("只允许上传 JPG、PNG、GIF 图片文件");
}2. 验证文件内容
不仅检查文件扩展名和 MIME 类型,还应该验证文件的实际内容,防止攻击者通过修改文件扩展名或 MIME 类型绕过检查。
php
// 验证图片文件
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$file_mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif'];
if (!in_array($file_mime, $allowed_mimes)) {
die("只允许上传 JPG、PNG、GIF 图片文件");
}
// 或者使用 getimagesize() 函数验证图片
$image_info = getimagesize($_FILES['file']['tmp_name']);
if (!$image_info) {
die("请上传有效的图片文件");
}3. 重命名上传文件
对上传的文件进行重命名,使用随机文件名,防止攻击者通过猜测文件名来访问上传的文件。
php
// 生成随机文件名
$random_name = uniqid() . '.' . $file_extension;
$upload_path = 'uploads/' . $random_name;
// 移动文件
if (move_uploaded_file($_FILES['file']['tmp_name'], $upload_path)) {
echo "文件上传成功";
} else {
echo "文件上传失败";
}4. 限制文件大小
限制上传文件的大小,防止上传过大的文件导致服务器资源耗尽。
php
// 限制文件大小为 2MB
$max_size = 2 * 1024 * 1024;
if ($_FILES['file']['size'] > $max_size) {
die("文件大小不能超过 2MB");
}5. 设置上传目录权限
设置上传目录的权限,确保上传的文件不能被执行,只能被读取。
bash
# 设置上传目录权限为 755(所有者可读写执行,其他用户只读执行)
chmod 755 uploads/
# 或者设置为 644(所有者可读写,其他用户只读)
chmod 644 uploads/6. 配置文件上传目录为不可执行
在 Apache 或 Nginx 配置中,设置上传目录为不可执行,防止上传的脚本文件被执行。
Apache 配置
apache
<Directory /path/to/uploads>
Options -ExecCGI
AddHandler cgi-script .php .pl .py .jsp
Deny from all
</Directory>Nginx 配置
nginx
location /uploads/ {
location ~ \.(php|pl|py|jsp)$ {
deny all;
}
}7. 使用安全的文件存储路径
将上传的文件存储在非 web 可访问的目录中,或者使用虚拟路径映射,防止直接访问上传的文件。
php
// 存储在非 web 可访问的目录
$upload_path = '/var/www/uploads/' . $random_name;
// 或者使用数据库存储文件路径,通过脚本读取文件
// 例如:download.php?id=1238. 定期清理上传文件
定期清理上传目录中的过期文件,防止存储空间被占用。
php
// 清理 30 天前的文件
$upload_dir = 'uploads/';
$files = scandir($upload_dir);
foreach ($files as $file) {
if ($file != '.' && $file != '..') {
$file_path = $upload_dir . $file;
if (filemtime($file_path) < time() - 30 * 24 * 3600) {
unlink($file_path);
}
}
}实战演练
场景:头像上传功能
不安全的实现
php
<?php
// 上传目录
$upload_dir = 'uploads/';
// 移动文件
$file_name = $_FILES['avatar']['name'];
$upload_path = $upload_dir . $file_name;
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $upload_path)) {
echo "头像上传成功";
} else {
echo "头像上传失败";
}
?>如果攻击者上传一个名为 shell.php 的文件,然后通过访问 http://example.com/uploads/shell.php 来执行恶意代码。
安全的实现
php
<?php
// 上传目录
$upload_dir = 'uploads/';
// 检查文件类型
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
$file_extension = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION));
if (!in_array($file_extension, $allowed_extensions)) {
die("只允许上传 JPG、PNG、GIF 图片文件");
}
// 验证文件内容
$image_info = getimagesize($_FILES['avatar']['tmp_name']);
if (!$image_info) {
die("请上传有效的图片文件");
}
// 限制文件大小
$max_size = 2 * 1024 * 1024;
if ($_FILES['avatar']['size'] > $max_size) {
die("文件大小不能超过 2MB");
}
// 生成随机文件名
$random_name = uniqid() . '.' . $file_extension;
$upload_path = $upload_dir . $random_name;
// 移动文件
if (move_uploaded_file($_FILES['avatar']['tmp_name'], $upload_path)) {
echo "头像上传成功";
} else {
echo "头像上传失败";
}
?>常见文件上传漏洞场景及防护
1. 前端验证绕过
攻击场景:攻击者绕过前端 JavaScript 验证,直接提交恶意文件。 防护方法:在服务器端进行验证,不要依赖前端验证。
2. MIME 类型欺骗
攻击场景:攻击者修改文件的 MIME 类型,绕过服务器的 MIME 类型检查。 防护方法:验证文件的实际内容,而不仅仅依赖 MIME 类型。
3. 文件名欺骗
攻击场景:攻击者使用特殊文件名(如 ../../../shell.php)进行路径遍历攻击。 防护方法:使用 basename() 函数获取文件名,防止路径遍历。
php
// 防止路径遍历
$file_name = basename($_FILES['file']['name']);4. 00 截断攻击
攻击场景:攻击者使用 shell.php%00.jpg 这样的文件名,利用 PHP 的 00 截断漏洞绕过文件扩展名检查。 防护方法:使用 PHP 5.3.4 及以上版本,或者对文件名进行处理,移除 00 字符。
php
// 移除 00 字符
$file_name = str_replace(chr(0), '', $_FILES['file']['name']);总结
文件上传漏洞是一种严重的安全漏洞,可能导致服务器被攻击、数据泄露等问题。通过限制文件类型、验证文件内容、重命名上传文件、限制文件大小、设置上传目录权限等方法,可以有效防止文件上传漏洞。
在开发过程中,应始终将安全放在首位,遵循安全编码实践,定期进行安全审计和测试,确保应用程序的安全性。
