未经许可,不得转载。
文章目录
- 文件上传漏洞
- 漏洞成因
- 未验证文件类型和扩展名
- 未限制文件上传路径
- 防范
- 验证文件类型和扩展名
- 验证文件内容
- 限制文件上传路径
- 使用安全的文件上传库
- 标准代码
文件上传漏洞
文件上传漏洞是指攻击者通过上传恶意文件(如可执行脚本、病毒、木马等)到服务器,从而执行恶意操作或获取服务器控制权的安全漏洞,一般发生在应用程序未对上传的文件进行严格的验证和限制时。
漏洞成因
未验证文件类型和扩展名
import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
@WebServlet("/upload") // 指定Servlet的URL映射
public class FileUploadServlet extends HttpServlet {
// 处理POST请求,实现文件上传
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查请求是否为多部分(即文件上传请求)
if (ServletFileUpload.isMultipartContent(request)) {
// 创建磁盘文件项工厂,用于处理文件上传
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// 解析请求,获取文件项列表
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
// 检查是否为文件字段,而不是普通表单字段
if (!item.isFormField()) {
// 获取上传文件的文件名
String fileName = new File(item.getName()).getName();
// 定义服务器上的文件存储路径(此处为/uploads/目录)
String filePath = "/uploads/" + fileName;
// 将上传的文件写入到服务器指定路径
item.write(new File(filePath));
// 向客户端返回上传成功的信息
response.getWriter().println("File uploaded: " + fileName);
}
}
} catch (Exception e) {
e.printStackTrace(); // 打印异常信息,方便调试
}
}
}
}
问题:未验证文件类型和扩展名,攻击者可以上传任意文件(如 .jsp
、.exe
等)。
未限制文件上传路径
String filePath = "/uploads/" + fileName;
item.write(new File(filePath));
问题:文件上传路径未做限制,攻击者可以通过构造特殊文件名(如 ../../malicious.jsp
)将文件上传到任意目录。
防范
验证文件类型和扩展名
- 使用白名单机制,只允许上传指定的文件类型(如
.jpg
、.png
、.pdf
等)。 - 不要依赖客户端验证(如 HTML 的
accept
属性),必须在服务器端进行验证。
String[] allowedExtensions = { "jpg", "png", "pdf" };
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
boolean isValidExtension = false; //默认为不合法类型
for (String ext : allowedExtensions) {
if (ext.equals(fileExtension)) {
isValidExtension = true; //若匹配到白名单,则合法。
break;
}
}
if (!isValidExtension) {
response.getWriter().println("Invalid file type.");
return;
}
验证文件内容
使用工具(如 Apache Tika
)验证文件内容是否与扩展名匹配。
import org.apache.tika.Tika;
Tika tika = new Tika();
String detectedType = tika.detect(new File(filePath));
if (!detectedType.startsWith("image/")) {
response.getWriter().println("Invalid file content.");
new File(filePath).delete(); // 删除非法文件
return;
}
限制文件上传路径
将文件上传路径限制在特定目录,避免攻击者通过路径遍历上传文件到任意目录。
String uploadDir = "/uploads/";
String filePath = uploadDir + randomFileName;
// 确保路径在允许的目录内
if (!filePath.startsWith(uploadDir)) {
response.getWriter().println("Invalid file path.");
return;
}
使用安全的文件上传库
使用经过验证的文件上传库(如 Apache Commons FileUpload),并确保库的版本是最新的。
标准代码
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.tika.Tika;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
private static final String UPLOAD_DIRECTORY = "/uploads"; // 上传目录
private static final String[] ALLOWED_EXTENSIONS = { "jpg", "jpeg", "png", "pdf" }; // 允许的文件扩展名
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查是否为文件上传请求
if (!ServletFileUpload.isMultipartContent(request)) {
response.getWriter().println("Request does not contain upload data.");
return;
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setSizeMax(MAX_FILE_SIZE); // 设置最大文件大小
try {
// 解析请求
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
// 判断当前的 FileItem 是否是一个普通的表单字段,如果不是,则执行后续的文件上传处理逻辑。
// 获取文件名
String fileName = new File(item.getName()).getName();
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
// 验证文件扩展名
if (!isAllowedExtension(fileExtension)) {
response.getWriter().println("Invalid file type. Allowed types: " + String.join(", ", ALLOWED_EXTENSIONS));
return;
}
// 生成随机文件名
String randomFileName = UUID.randomUUID().toString() + "." + fileExtension;
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
File uploadDir = new File(uploadPath);
// 创建上传目录(如果不存在)
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
// 保存文件
String filePath = uploadPath + File.separator + randomFileName;
File storeFile = new File(filePath);
item.write(storeFile);
// 验证文件内容
if (!isValidFileContent(storeFile, fileExtension)) {
response.getWriter().println("Invalid file content.");
storeFile.delete(); // 删除非法文件
return;
}
response.getWriter().println("File uploaded successfully: " + randomFileName);
}
}
} catch (Exception e) {
response.getWriter().println("Error occurred: " + e.getMessage());
}
}
/**
* 检查文件扩展名是否合法
*/
private boolean isAllowedExtension(String fileExtension) {
for (String ext : ALLOWED_EXTENSIONS) {
if (ext.equalsIgnoreCase(fileExtension)) {
return true;
}
}
return false;
}
/**
* 验证文件内容是否合法
*/
private boolean isValidFileContent(File file, String expectedExtension) throws IOException {
Tika tika = new Tika();
String detectedType = tika.detect(file);
// 根据文件扩展名验证内容类型
switch (expectedExtension.toLowerCase()) {
case "jpg":
case "jpeg":
return detectedType.equals("image/jpeg");
case "png":
return detectedType.equals("image/png");
case "pdf":
return detectedType.equals("application/pdf");
default:
return false;
}
}
}