You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

90 lines
3.6 KiB

<%@ page import="java.net.URLEncoder" %>
<%@ page import="java.io.FileInputStream" %>
<%@ page import="org.slf4j.Logger" %>
<%@ page import="org.slf4j.LoggerFactory" %>
<%@ page import="java.nio.file.Paths" %>
<%@ page import="java.nio.file.Path" %>
<%
// 日志初始化
Logger logger = LoggerFactory.getLogger("download.jsp");
// 重置响应,避免乱码或缓存问题
response.reset();
response.setContentType("application/x-download");
// 1. 定义允许的根目录(限制为应用部署目录下的指定子目录,如downloads)
String appRoot = application.getRealPath("/");
// 安全增强:强制文件只能从应用内的downloads目录下载(根据实际业务调整)
String allowedRoot = appRoot + "downloads" + java.io.File.separator;
// 2. 从session获取参数(需校验,即使是session存储也可能被篡改)
String basepath = (String) session.getAttribute("basepath");
String filename = (String) session.getAttribute("filename");
// 3. 输入验证:校验basepath和filename是否合法
if (basepath == null || basepath.trim().isEmpty() || filename == null || filename.trim().isEmpty()) {
logger.error("文件路径或名称为空,拒绝下载");
response.sendError(400, "无效的请求参数");
return;
}
// 4. 净化参数:移除危险字符,禁止路径跳转序列
String safeBasepath = basepath.trim().replace("..", "").replace("/", "").replace("\\", "");
String safeFilename = filename.trim().replace("..", "").replace("/", "").replace("\\", "");
// 5. 构建完整路径并规范化(解析相对路径符号)
String filedownload = allowedRoot + safeBasepath + java.io.File.separator + safeFilename;
Path resolvedPath;
Path rootPath;
try {
resolvedPath = Paths.get(filedownload).toRealPath(); // 规范化路径,消除../等
rootPath = Paths.get(allowedRoot).toRealPath(); // 规范化根目录
} catch (Exception e) {
logger.error("路径解析失败,可能存在非法路径: " + filedownload, e);
response.sendError(403, "文件访问被拒绝");
return;
}
// 6. 校验最终路径是否在允许的根目录内(核心防护)
if (!resolvedPath.startsWith(rootPath)) {
logger.error("检测到路径遍历攻击,尝试访问未授权文件: " + resolvedPath);
response.sendError(403, "文件访问被拒绝");
return;
}
// 7. 设置下载响应头(处理文件名编码)
try {
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(safeFilename, "UTF-8"));
} catch (Exception e) {
logger.error("文件名编码失败", e);
response.sendError(500, "服务器处理错误");
return;
}
// 8. 安全输出文件流
java.io.OutputStream outp = null;
java.io.FileInputStream in = null;
try {
outp = response.getOutputStream();
in = new FileInputStream(resolvedPath.toFile()); // 使用校验后的安全路径
byte[] b = new byte[1024 * 1024];
int i;
while ((i = in.read(b)) > 0) {
outp.write(b, 0, i);
}
outp.flush();
// 清理缓冲区,避免 IllegalStateException
out.clear();
out = pageContext.pushBody();
} catch (Exception e) {
logger.error("文件下载失败", e);
response.sendError(500, "文件下载失败");
} finally {
if (in != null) {
try { in.close(); } catch (Exception e) { logger.error("输入流关闭失败", e); }
}
// 不关闭输出流,由容器处理
}
%>