Skip to content

16.3 防文件上传漏洞

文件上传漏洞原理

文件上传漏洞是指攻击者通过上传恶意文件到服务器,从而执行恶意代码、获取服务器权限或破坏网站的安全漏洞。这种漏洞通常出现在允许用户上传文件的功能中,如头像上传、附件上传等。

攻击示例

  1. 上传恶意脚本:攻击者上传包含恶意代码的 PHP、ASP、JSP 等文件,然后通过访问该文件执行恶意代码。
  2. 上传 Webshell:攻击者上传 Webshell(网站后门),获得服务器的控制权。
  3. 上传恶意文件:攻击者上传病毒、木马等恶意文件,危害服务器安全。
  4. 绕过文件类型检查:攻击者通过修改文件扩展名、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=123

8. 定期清理上传文件

定期清理上传目录中的过期文件,防止存储空间被占用。

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']);

总结

文件上传漏洞是一种严重的安全漏洞,可能导致服务器被攻击、数据泄露等问题。通过限制文件类型、验证文件内容、重命名上传文件、限制文件大小、设置上传目录权限等方法,可以有效防止文件上传漏洞。

在开发过程中,应始终将安全放在首位,遵循安全编码实践,定期进行安全审计和测试,确保应用程序的安全性。

© 2026 编程马·菜鸟教程 版权所有