Commit 6bb166b89f7dbdf441e51114d3166ab95371220b

Authored by RuoYi
1 parent 823e9566

阻止任意文件下载漏洞

ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java
... ... @@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse;
5 5 import org.slf4j.Logger;
6 6 import org.slf4j.LoggerFactory;
7 7 import org.springframework.beans.factory.annotation.Autowired;
  8 +import org.springframework.http.MediaType;
8 9 import org.springframework.web.bind.annotation.GetMapping;
9 10 import org.springframework.web.bind.annotation.PostMapping;
10 11 import org.springframework.web.bind.annotation.RestController;
... ... @@ -41,17 +42,15 @@ public class CommonController
41 42 {
42 43 try
43 44 {
44   - if (!FileUtils.isValidFilename(fileName))
  45 + if (!FileUtils.checkAllowDownload(fileName))
45 46 {
46 47 throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
47 48 }
48 49 String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
49 50 String filePath = RuoYiConfig.getDownloadPath() + fileName;
50 51  
51   - response.setCharacterEncoding("utf-8");
52   - response.setContentType("multipart/form-data");
53   - response.setHeader("Content-Disposition",
54   - "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, realFileName));
  52 + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
  53 + FileUtils.setAttachmentResponseHeader(response, realFileName);
55 54 FileUtils.writeBytes(filePath, response.getOutputStream());
56 55 if (delete)
57 56 {
... ... @@ -92,18 +91,28 @@ public class CommonController
92 91 * 本地资源通用下载
93 92 */
94 93 @GetMapping("/common/download/resource")
95   - public void resourceDownload(String name, HttpServletRequest request, HttpServletResponse response) throws Exception
  94 + public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
  95 + throws Exception
96 96 {
97   - // 本地资源路径
98   - String localPath = RuoYiConfig.getProfile();
99   - // 数据库资源地址
100   - String downloadPath = localPath + StringUtils.substringAfter(name, Constants.RESOURCE_PREFIX);
101   - // 下载名称
102   - String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
103   - response.setCharacterEncoding("utf-8");
104   - response.setContentType("multipart/form-data");
105   - response.setHeader("Content-Disposition",
106   - "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, downloadName));
107   - FileUtils.writeBytes(downloadPath, response.getOutputStream());
  97 + try
  98 + {
  99 + if (!FileUtils.checkAllowDownload(resource))
  100 + {
  101 + throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
  102 + }
  103 + // 本地资源路径
  104 + String localPath = RuoYiConfig.getProfile();
  105 + // 数据库资源地址
  106 + String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
  107 + // 下载名称
  108 + String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
  109 + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
  110 + FileUtils.setAttachmentResponseHeader(response, downloadName);
  111 + FileUtils.writeBytes(downloadPath, response.getOutputStream());
  112 + }
  113 + catch (Exception e)
  114 + {
  115 + log.error("下载文件失败", e);
  116 + }
108 117 }
109 118 }
... ...
ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileTypeUtils.java 0 → 100644
  1 +package com.ruoyi.common.utils.file;
  2 +
  3 +import java.io.File;
  4 +import org.apache.commons.lang3.StringUtils;
  5 +
  6 +/**
  7 + * 文件类型工具类
  8 + *
  9 + * @author ruoyi
  10 + */
  11 +public class FileTypeUtils
  12 +{
  13 + /**
  14 + * 获取文件类型
  15 + * <p>
  16 + * 例如: ruoyi.txt, 返回: txt
  17 + *
  18 + * @param file 文件名
  19 + * @return 后缀(不含".")
  20 + */
  21 + public static String getFileType(File file)
  22 + {
  23 + if (null == file)
  24 + {
  25 + return StringUtils.EMPTY;
  26 + }
  27 + return getFileType(file.getName());
  28 + }
  29 +
  30 + /**
  31 + * 获取文件类型
  32 + * <p>
  33 + * 例如: ruoyi.txt, 返回: txt
  34 + *
  35 + * @param fileName 文件名
  36 + * @return 后缀(不含".")
  37 + */
  38 + public static String getFileType(String fileName)
  39 + {
  40 + int separatorIndex = fileName.lastIndexOf(".");
  41 + if (separatorIndex < 0)
  42 + {
  43 + return "";
  44 + }
  45 + return fileName.substring(separatorIndex + 1).toLowerCase();
  46 + }
  47 +}
0 48 \ No newline at end of file
... ...
ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java
... ... @@ -7,7 +7,11 @@ import java.io.IOException;
7 7 import java.io.OutputStream;
8 8 import java.io.UnsupportedEncodingException;
9 9 import java.net.URLEncoder;
  10 +import java.nio.charset.StandardCharsets;
10 11 import javax.servlet.http.HttpServletRequest;
  12 +import javax.servlet.http.HttpServletResponse;
  13 +import org.apache.commons.lang3.ArrayUtils;
  14 +import com.ruoyi.common.utils.StringUtils;
11 15  
12 16 /**
13 17 * 文件处理工具类
... ... @@ -105,14 +109,37 @@ public class FileUtils extends org.apache.commons.io.FileUtils
105 109 }
106 110  
107 111 /**
  112 + * 检查文件是否可下载
  113 + *
  114 + * @param resource 需要下载的文件
  115 + * @return true 正常 false 非法
  116 + */
  117 + public static boolean checkAllowDownload(String resource)
  118 + {
  119 + // 禁止目录上跳级别
  120 + if (StringUtils.contains(resource, ".."))
  121 + {
  122 + return false;
  123 + }
  124 +
  125 + // 检查允许下载的文件规则
  126 + if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource)))
  127 + {
  128 + return true;
  129 + }
  130 +
  131 + // 不在允许下载的文件规则
  132 + return false;
  133 + }
  134 +
  135 + /**
108 136 * 下载文件名重新编码
109 137 *
110 138 * @param request 请求对象
111 139 * @param fileName 文件名
112 140 * @return 编码后的文件名
113 141 */
114   - public static String setFileDownloadHeader(HttpServletRequest request, String fileName)
115   - throws UnsupportedEncodingException
  142 + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException
116 143 {
117 144 final String agent = request.getHeader("USER-AGENT");
118 145 String filename = fileName;
... ... @@ -139,4 +166,38 @@ public class FileUtils extends org.apache.commons.io.FileUtils
139 166 }
140 167 return filename;
141 168 }
  169 +
  170 + /**
  171 + * 下载文件名重新编码
  172 + *
  173 + * @param response 响应对象
  174 + * @param realFileName 真实文件名
  175 + * @return
  176 + */
  177 + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException
  178 + {
  179 + String percentEncodedFileName = percentEncode(realFileName);
  180 +
  181 + StringBuilder contentDispositionValue = new StringBuilder();
  182 + contentDispositionValue.append("attachment; filename=")
  183 + .append(percentEncodedFileName)
  184 + .append(";")
  185 + .append("filename*=")
  186 + .append("utf-8''")
  187 + .append(percentEncodedFileName);
  188 +
  189 + response.setHeader("Content-disposition", contentDispositionValue.toString());
  190 + }
  191 +
  192 + /**
  193 + * 百分号编码工具方法
  194 + *
  195 + * @param s 需要百分号编码的字符串
  196 + * @return 百分号编码后的字符串
  197 + */
  198 + public static String percentEncode(String s) throws UnsupportedEncodingException
  199 + {
  200 + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString());
  201 + return encode.replaceAll("\\+", "%20");
  202 + }
142 203 }
... ...