阻止任意文件下载漏洞
This commit is contained in:
		| @@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 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.PostMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| @@ -41,17 +42,15 @@ public class CommonController | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             if (!FileUtils.isValidFilename(fileName)) | ||||
|             if (!FileUtils.checkAllowDownload(fileName)) | ||||
|             { | ||||
|                 throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName)); | ||||
|             } | ||||
|             String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1); | ||||
|             String filePath = RuoYiConfig.getDownloadPath() + fileName; | ||||
|  | ||||
|             response.setCharacterEncoding("utf-8"); | ||||
|             response.setContentType("multipart/form-data"); | ||||
|             response.setHeader("Content-Disposition", | ||||
|                     "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, realFileName)); | ||||
|             response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); | ||||
|             FileUtils.setAttachmentResponseHeader(response, realFileName); | ||||
|             FileUtils.writeBytes(filePath, response.getOutputStream()); | ||||
|             if (delete) | ||||
|             { | ||||
| @@ -92,18 +91,28 @@ public class CommonController | ||||
|      * 本地资源通用下载 | ||||
|      */ | ||||
|     @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 | ||||
|     { | ||||
|         // 本地资源路径 | ||||
|         String localPath = RuoYiConfig.getProfile(); | ||||
|         // 数据库资源地址 | ||||
|         String downloadPath = localPath + StringUtils.substringAfter(name, Constants.RESOURCE_PREFIX); | ||||
|         // 下载名称 | ||||
|         String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); | ||||
|         response.setCharacterEncoding("utf-8"); | ||||
|         response.setContentType("multipart/form-data"); | ||||
|         response.setHeader("Content-Disposition", | ||||
|                 "attachment;fileName=" + FileUtils.setFileDownloadHeader(request, downloadName)); | ||||
|         FileUtils.writeBytes(downloadPath, response.getOutputStream()); | ||||
|         try | ||||
|         { | ||||
|             if (!FileUtils.checkAllowDownload(resource)) | ||||
|             { | ||||
|                 throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); | ||||
|             } | ||||
|             // 本地资源路径 | ||||
|             String localPath = RuoYiConfig.getProfile(); | ||||
|             // 数据库资源地址 | ||||
|             String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); | ||||
|             // 下载名称 | ||||
|             String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); | ||||
|             response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); | ||||
|             FileUtils.setAttachmentResponseHeader(response, downloadName); | ||||
|             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.UnsupportedEncodingException; | ||||
| import java.net.URLEncoder; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| 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); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查文件是否可下载 | ||||
|      *  | ||||
|      * @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 文件名 | ||||
|      * @return 编码后的文件名 | ||||
|      */ | ||||
|     public static String setFileDownloadHeader(HttpServletRequest request, String fileName) | ||||
|             throws UnsupportedEncodingException | ||||
|     public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException | ||||
|     { | ||||
|         final String agent = request.getHeader("USER-AGENT"); | ||||
|         String filename = fileName; | ||||
| @@ -139,4 +166,38 @@ public class FileUtils extends org.apache.commons.io.FileUtils | ||||
|         } | ||||
|         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