Commit 6bb166b89f7dbdf441e51114d3166ab95371220b
1 parent
823e9566
阻止任意文件下载漏洞
Showing
3 changed files
with
136 additions
and
19 deletions
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 | } | ... | ... |