文件上传漏洞与防御
一、什么是文件上传漏洞
文件上传漏洞是指网站在接收用户上传文件时,后端未对文件类型、文件名、文件内容做严格安全校验,导致攻击者可上传恶意脚本(如 php、jsp、asp、aspx、py 等),并被服务器正常解析执行,最终获取服务器权限的高危漏洞。
常见场景:头像上传、附件上传、图片上传、文档上传、封面 / 素材上传等。
漏洞危害
- 直接获取网站 WebShell,控制网站
- 服务器被入侵,进行内网渗透
- 删库、篡改页面、挂黑页、挖矿、勒索病毒
- 敏感数据泄露、服务器被当作肉鸡
二、漏洞产生的根本原因
- 只依赖前端 JS 校验,后端完全不校验
- 只校验文件后缀,不校验文件头与真实内容
- 使用黑名单过滤,规则不完整,极易被绕过
- 服务器开启危险解析,上传目录可执行脚本
- 文件名、路径用户可控,导致路径穿越
- 上传目录权限过大,文件可直接被解析运行
- 未对文件重命名、未做内容安全检测
三、常见上传绕过方式
1. 前端绕过
前端通过 JS 限制后缀,攻击者抓包修改文件名与类型即可绕过。
2. 文件后缀绕过
- 大小写绕过:PhP、AspX、Jsp
- 多后缀绕过:shell.php.jpg
- 特殊字符截断:shell.php;.jpg、shell.php%00.jpg
- 解析漏洞绕过:shell.php/.、shell.jpg/.php
3. MIME 类型绕过
抓包修改请求头:
Content-Type: image/jpeg4. 文件内容绕过
- 图片马:图片文件中插入一句话木马
- 伪造文件头:添加 GIF89a 等图片标识伪装图片
5. 黑名单绕过
黑名单遗漏危险后缀,新版本可解析:php5、php7、phtml、phps、asa、cer、aspx 等。
四、完整安全防御方案(最稳通用方案)
1. 后端强校验(核心)
- 必须使用白名单,严禁使用黑名单
- 校验内容:文件后缀 + MIME + 文件头 + 文件内容
- 禁止上传可执行脚本:php、jsp、asp、aspx、py、sh、exe 等
2. 文件重命名
- 上传后统一使用随机字符串重命名,如 UUID、uniqid
- 不使用用户上传的原始文件名,防止路径穿越、文件覆盖
3. 独立存储 & 关闭脚本解析
- 上传目录与 Web 目录分离
- 上传目录禁止解析任何脚本
- 优先使用 OSS、对象存储、CDN 存储用户文件
4. 文件内容检测
- 读取文件头,验证是否为真实图片 / 文档
- 图片进行二次渲染 / 重生成,破坏嵌入的恶意代码
- 限制文件大小、上传频率、单次上传数量
5. 服务器安全配置
- Nginx/Apache 配置上传目录禁止解析脚本
- 关闭不必要的解析规则与危险函数
- Web 服务以最小权限运行
- 配合 WAF、病毒查杀、上传日志审计
五、一句话防御口诀
前端校验不可信,后端白名单必做。文件头 + 内容双检查,随机改名存别处。上传目录禁执行,服务器配置要严格。
六、实战防御代码示例
1. PHP 安全上传(完整可运行)
<?php
function safeUploadFile() {
$allowedExts = array('jpg', 'jpeg', 'png', 'gif');
$allowedMime = array('image/jpeg', 'image/png', 'image/gif');
$maxSize = 2 * 1024 * 1024;
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
return "上传失败:文件上传出错";
}
if ($_FILES['file']['size'] > $maxSize) {
return "上传失败:文件超过2MB";
}
$fileInfo = pathinfo($_FILES['file']['name']);
$fileExt = strtolower($fileInfo['extension']);
$fileMime = mime_content_type($_FILES['file']['tmp_name']);
if (!in_array($fileExt, $allowedExts) || !in_array($fileMime, $allowedMime)) {
return "仅允许上传 jpg/png/gif 图片";
}
$fileHeader = file_get_contents($_FILES['file']['tmp_name'], false, null, 0, 4);
$allowedHeaders = array(
"\xFF\xD8\xFF\xE0",
"\x89\x50\x4E\x47",
"GIF89a"
);
if (!in_array($fileHeader, $allowedHeaders)) {
return "文件不是合法图片";
}
$newFileName = uniqid('upload_', true) . '.' . $fileExt;
$uploadPath = './uploads/';
if (!is_dir($uploadPath)) {
mkdir($uploadPath, 0755, true);
}
if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadPath . $newFileName)) {
return "上传成功:{$uploadPath}{$newFileName}";
} else {
return "上传失败:文件移动出错";
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
echo safeUploadFile();
}
?>
<form method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上传">
</form>Nginx 禁止上传目录解析脚本
location /uploads/ {
location ~ \.php$ {
deny all;
}
location ~* \.(jpg|jpeg|png|gif)$ {
expires 30d;
}
}2. Java SpringBoot 安全上传
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@RestController
public class SafeUploadController {
private static final String[] ALLOWED_EXT = {"jpg", "jpeg", "png", "gif"};
private static final long MAX_SIZE = 2 * 1024 * 1024;
private static final String UPLOAD_PATH = "./uploads/";
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "上传失败:文件为空";
}
if (file.getSize() > MAX_SIZE) {
return "上传失败:文件超过2MB";
}
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase();
boolean extAllowed = false;
for (String allowed : ALLOWED_EXT) {
if (allowed.equals(ext)) {
extAllowed = true;
break;
}
}
if (!extAllowed) {
return "仅允许上传 jpg/png/gif 图片";
}
try {
BufferedImage image = ImageIO.read(file.getInputStream());
if (image == null || image.getWidth() <= 0) {
return "文件不是合法图片";
}
} catch (IOException e) {
return "图片校验失败";
}
String newFileName = UUID.randomUUID() + "." + ext;
File uploadDir = new File(UPLOAD_PATH);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
try {
file.transferTo(new File(UPLOAD_PATH + newFileName));
return "上传成功:" + UPLOAD_PATH + newFileName;
} catch (IOException e) {
return "上传失败:" + e.getMessage();
}
}
}七、核心总结
- 文件上传防御的核心是:白名单 + 多层校验 + 随机重命名
- 前端校验不可信,所有校验必须在后端实现
- 上传目录必须禁止脚本解析,即使上传木马也无法执行
- 优先使用独立存储(OSS/CDN),从架构上避免解析风险
版权所属:SO JSON在线解析
原文地址:https://www.sojson.com/blog/588.html
转载时必须以链接形式注明原始出处及本声明。
如果本文对你有帮助,那么请你赞助我,让我更有激情的写下去,帮助更多的人。
