阻止任意文件下载漏洞
This commit is contained in:
		| @@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse; | |||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
| import org.springframework.web.bind.annotation.GetMapping; | import org.springframework.web.bind.annotation.GetMapping; | ||||||
| import org.springframework.web.bind.annotation.PostMapping; | import org.springframework.web.bind.annotation.PostMapping; | ||||||
| import org.springframework.web.bind.annotation.RestController; | import org.springframework.web.bind.annotation.RestController; | ||||||
| @@ -41,17 +42,15 @@ public class CommonController | |||||||
|     { |     { | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|             if (!FileUtils.isValidFilename(fileName)) |             if (!FileUtils.checkAllowDownload(fileName)) | ||||||
|             { |             { | ||||||
|                 throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); |                 throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); | ||||||
|             } |             } | ||||||
|             String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); |             String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); | ||||||
|             String filePath = RuoYiConfig.getDownloadPath() + fileName; |             String filePath = RuoYiConfig.getDownloadPath() + fileName; | ||||||
|  |  | ||||||
|             response.setCharacterEncoding("utf-8"); |             response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); | ||||||
|             response.setContentType("multipart/form-data"); |             FileUtils.setAttachmentResponseHeader(response, realFileName); | ||||||
|             response.setHeader("Content-Disposition", |  | ||||||
|                     "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, realFileName)); |  | ||||||
|             FileUtils.writeBytes(filePath, response.getOutputStream()); |             FileUtils.writeBytes(filePath, response.getOutputStream()); | ||||||
|             if (delete) |             if (delete) | ||||||
|             { |             { | ||||||
| @@ -92,18 +91,28 @@ public class CommonController | |||||||
|      * 本地资源通用下载 |      * 本地资源通用下载 | ||||||
|      */ |      */ | ||||||
|     @GetMapping("/common/download/resource") |     @GetMapping("/common/download/resource") | ||||||
|     public void resourceDownload(String name, HttpServletRequest request, HttpServletResponse response) throws Exception |     public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) | ||||||
|  |             throws Exception | ||||||
|     { |     { | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             if (!FileUtils.checkAllowDownload(resource)) | ||||||
|  |             { | ||||||
|  |                 throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); | ||||||
|  |             } | ||||||
|             // 本地资源路径 |             // 本地资源路径 | ||||||
|             String localPath = RuoYiConfig.getProfile(); |             String localPath = RuoYiConfig.getProfile(); | ||||||
|             // 数据库资源地址 |             // 数据库资源地址 | ||||||
|         String downloadPath = localPath + StringUtils.substringAfter(name, Constants.RESOURCE_PREFIX); |             String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); | ||||||
|             // 下载名称 |             // 下载名称 | ||||||
|             String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); |             String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); | ||||||
|         response.setCharacterEncoding("utf-8"); |             response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); | ||||||
|         response.setContentType("multipart/form-data"); |             FileUtils.setAttachmentResponseHeader(response, downloadName); | ||||||
|         response.setHeader("Content-Disposition", |  | ||||||
|                 "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, downloadName)); |  | ||||||
|             FileUtils.writeBytes(downloadPath, response.getOutputStream()); |             FileUtils.writeBytes(downloadPath, response.getOutputStream()); | ||||||
|         } |         } | ||||||
|  |         catch (Exception e) | ||||||
|  |         { | ||||||
|  |             log.error("下载文件失败", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,47 @@ | |||||||
|  | package com.ruoyi.common.utils.file; | ||||||
|  |  | ||||||
|  | import java.io.File; | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 文件类型工具类 | ||||||
|  |  * | ||||||
|  |  * @author ruoyi | ||||||
|  |  */ | ||||||
|  | public class FileTypeUtils | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * 获取文件类型 | ||||||
|  |      * <p> | ||||||
|  |      * 例如: ruoyi.txt, 返回: txt | ||||||
|  |      *  | ||||||
|  |      * @param file 文件名 | ||||||
|  |      * @return 后缀(不含".") | ||||||
|  |      */ | ||||||
|  |     public static String getFileType(File file) | ||||||
|  |     { | ||||||
|  |         if (null == file) | ||||||
|  |         { | ||||||
|  |             return StringUtils.EMPTY; | ||||||
|  |         } | ||||||
|  |         return getFileType(file.getName()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 获取文件类型 | ||||||
|  |      * <p> | ||||||
|  |      * 例如: ruoyi.txt, 返回: txt | ||||||
|  |      * | ||||||
|  |      * @param fileName 文件名 | ||||||
|  |      * @return 后缀(不含".") | ||||||
|  |      */ | ||||||
|  |     public static String getFileType(String fileName) | ||||||
|  |     { | ||||||
|  |         int separatorIndex = fileName.lastIndexOf("."); | ||||||
|  |         if (separatorIndex < 0) | ||||||
|  |         { | ||||||
|  |             return ""; | ||||||
|  |         } | ||||||
|  |         return fileName.substring(separatorIndex + 1).toLowerCase(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -7,7 +7,11 @@ import java.io.IOException; | |||||||
| import java.io.OutputStream; | import java.io.OutputStream; | ||||||
| import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||||
| import java.net.URLEncoder; | import java.net.URLEncoder; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
| import javax.servlet.http.HttpServletRequest; | import javax.servlet.http.HttpServletRequest; | ||||||
|  | import javax.servlet.http.HttpServletResponse; | ||||||
|  | import org.apache.commons.lang3.ArrayUtils; | ||||||
|  | import com.ruoyi.common.utils.StringUtils; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * 文件处理工具类 |  * 文件处理工具类 | ||||||
| @@ -104,6 +108,30 @@ public class FileUtils extends org.apache.commons.io.FileUtils | |||||||
|         return filename.matches(FILENAME_PATTERN); |         return filename.matches(FILENAME_PATTERN); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 检查文件是否可下载 | ||||||
|  |      *  | ||||||
|  |      * @param resource 需要下载的文件 | ||||||
|  |      * @return true 正常 false 非法 | ||||||
|  |      */ | ||||||
|  |     public static boolean checkAllowDownload(String resource) | ||||||
|  |     { | ||||||
|  |         // 禁止目录上跳级别 | ||||||
|  |         if (StringUtils.contains(resource, "..")) | ||||||
|  |         { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 检查允许下载的文件规则 | ||||||
|  |         if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) | ||||||
|  |         { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // 不在允许下载的文件规则 | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * 下载文件名重新编码 |      * 下载文件名重新编码 | ||||||
|      *  |      *  | ||||||
| @@ -111,8 +139,7 @@ public class FileUtils extends org.apache.commons.io.FileUtils | |||||||
|      * @param fileName 文件名 |      * @param fileName 文件名 | ||||||
|      * @return 编码后的文件名 |      * @return 编码后的文件名 | ||||||
|      */ |      */ | ||||||
|     public static String setFileDownloadHeader(HttpServletRequest request, String fileName) |     public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException | ||||||
|             throws UnsupportedEncodingException |  | ||||||
|     { |     { | ||||||
|         final String agent = request.getHeader("USER-AGENT"); |         final String agent = request.getHeader("USER-AGENT"); | ||||||
|         String filename = fileName; |         String filename = fileName; | ||||||
| @@ -139,4 +166,38 @@ public class FileUtils extends org.apache.commons.io.FileUtils | |||||||
|         } |         } | ||||||
|         return filename; |         return filename; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 下载文件名重新编码 | ||||||
|  |      * | ||||||
|  |      * @param response 响应对象 | ||||||
|  |      * @param realFileName 真实文件名 | ||||||
|  |      * @return | ||||||
|  |      */ | ||||||
|  |     public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException | ||||||
|  |     { | ||||||
|  |         String percentEncodedFileName = percentEncode(realFileName); | ||||||
|  |  | ||||||
|  |         StringBuilder contentDispositionValue = new StringBuilder(); | ||||||
|  |         contentDispositionValue.append("attachment; filename=") | ||||||
|  |                 .append(percentEncodedFileName) | ||||||
|  |                 .append(";") | ||||||
|  |                 .append("filename*=") | ||||||
|  |                 .append("utf-8''") | ||||||
|  |                 .append(percentEncodedFileName); | ||||||
|  |  | ||||||
|  |         response.setHeader("Content-disposition", contentDispositionValue.toString()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 百分号编码工具方法 | ||||||
|  |      * | ||||||
|  |      * @param s 需要百分号编码的字符串 | ||||||
|  |      * @return 百分号编码后的字符串 | ||||||
|  |      */ | ||||||
|  |     public static String percentEncode(String s) throws UnsupportedEncodingException | ||||||
|  |     { | ||||||
|  |         String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); | ||||||
|  |         return encode.replaceAll("\\+", "%20"); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user