作者 RuoYi

阻止任意文件下载漏洞

@@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse; @@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse;
5 import org.slf4j.Logger; 5 import org.slf4j.Logger;
6 import org.slf4j.LoggerFactory; 6 import org.slf4j.LoggerFactory;
7 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.beans.factory.annotation.Autowired;
  8 +import org.springframework.http.MediaType;
8 import org.springframework.web.bind.annotation.GetMapping; 9 import org.springframework.web.bind.annotation.GetMapping;
9 import org.springframework.web.bind.annotation.PostMapping; 10 import org.springframework.web.bind.annotation.PostMapping;
10 import org.springframework.web.bind.annotation.RestController; 11 import org.springframework.web.bind.annotation.RestController;
@@ -41,17 +42,15 @@ public class CommonController @@ -41,17 +42,15 @@ public class CommonController
41 { 42 {
42 try 43 try
43 { 44 {
44 - if (!FileUtils.isValidFilename(fileName)) 45 + if (!FileUtils.checkAllowDownload(fileName))
45 { 46 {
46 throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); 47 throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
47 } 48 }
48 String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); 49 String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
49 String filePath = RuoYiConfig.getDownloadPath() + fileName; 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 FileUtils.writeBytes(filePath, response.getOutputStream()); 54 FileUtils.writeBytes(filePath, response.getOutputStream());
56 if (delete) 55 if (delete)
57 { 56 {
@@ -92,18 +91,28 @@ public class CommonController @@ -92,18 +91,28 @@ public class CommonController
92 * 本地资源通用下载 91 * 本地资源通用下载
93 */ 92 */
94 @GetMapping("/common/download/resource") 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 }
  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 +}
@@ -7,7 +7,11 @@ import java.io.IOException; @@ -7,7 +7,11 @@ import java.io.IOException;
7 import java.io.OutputStream; 7 import java.io.OutputStream;
8 import java.io.UnsupportedEncodingException; 8 import java.io.UnsupportedEncodingException;
9 import java.net.URLEncoder; 9 import java.net.URLEncoder;
  10 +import java.nio.charset.StandardCharsets;
10 import javax.servlet.http.HttpServletRequest; 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,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 * @param request 请求对象 138 * @param request 请求对象
111 * @param fileName 文件名 139 * @param fileName 文件名
112 * @return 编码后的文件名 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 final String agent = request.getHeader("USER-AGENT"); 144 final String agent = request.getHeader("USER-AGENT");
118 String filename = fileName; 145 String filename = fileName;
@@ -139,4 +166,38 @@ public class FileUtils extends org.apache.commons.io.FileUtils @@ -139,4 +166,38 @@ public class FileUtils extends org.apache.commons.io.FileUtils
139 } 166 }
140 return filename; 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 }