Browse Source

高危:任意文件上传。
添加文件校验,在不同的上传文件场景进行上传文件的校验。

大数据与最优化研究所 4 months ago
parent
commit
84ab851340

+ 31 - 0
src/main/java/com/xjrsoft/common/constant/GlobalConstant.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.ListUtil;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @Author: tzx
@@ -519,4 +520,34 @@ public interface GlobalConstant {
      * oauth2 缓存key
      */
     String OAUTH2  = "oauth2:";
+
+    // 允许的文件扩展名与MIME类型映射
+    public static final Map<String, String> ALLOWED_FILE_TYPES = Map.ofEntries(
+            // 图片类型
+            Map.entry("jpg", "image/jpeg"),
+            Map.entry("jpeg", "image/jpeg"),
+            Map.entry("png", "image/png"),
+            Map.entry("gif", "image/gif"),
+            Map.entry("webp", "image/webp"),
+
+            // 文档类型
+            Map.entry("pdf", "application/pdf"),
+            Map.entry("doc", "application/msword"),
+            Map.entry("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
+            Map.entry("xls", "application/vnd.ms-excel"),
+            Map.entry("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
+
+            // 音频类型
+            Map.entry("mp3", "audio/mpeg"),
+            Map.entry("wav", "audio/wav"),
+            Map.entry("ogg", "audio/ogg"),
+            Map.entry("aac", "audio/aac"),
+
+            // 视频类型
+            Map.entry("mp4", "video/mp4"),
+            Map.entry("mov", "video/quicktime"),
+            Map.entry("avi", "video/x-msvideo"),
+            Map.entry("webm", "video/webm")
+    );
+
 }

+ 114 - 70
src/main/java/com/xjrsoft/common/utils/UploadUtil.java

@@ -1,70 +1,114 @@
-package com.xjrsoft.common.utils;
-
-import com.baomidou.mybatisplus.core.toolkit.StringPool;
-import com.xjrsoft.common.factory.OssFactory;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.ResponseBody;
-import org.apache.commons.collections.CollectionUtils;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-public class UploadUtil {
-
-    //单文件上传
-    public static String  uploadFile(MultipartFile file) throws Exception {
-        if (file.isEmpty()) {
-            throw new RuntimeException("上传文件不能为空");
-        }
-        //上传文件
-        String suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf(StringPool.DOT));
-        return Objects.requireNonNull(OssFactory.build()).uploadSuffix(file.getBytes(), suffix);
-    }
-
-    //多文件上传
-    public  static List<String> uploadFiles(List<MultipartFile> files) throws Exception {
-        if (CollectionUtils.isNotEmpty(files)) {
-            throw new RuntimeException("上传文件不能为空");
-        }
-        List<String> urls = new ArrayList<>();
-        //上传文件
-        for (MultipartFile file : files) {
-            urls.add(uploadFile(file));
-        }
-        return urls;
-    }
-
-    public static InputStream download(String path) {
-        try {
-            OkHttpClient client = new OkHttpClient();
-            Request req = new Request.Builder().url(path).build();
-            InputStream inputStream = null;
-                okhttp3.Response resp = client.newCall(req).execute();
-                if (resp.isSuccessful()) {
-                    ResponseBody body = resp.body();
-                    inputStream = body.byteStream();
-                }
-            return inputStream;
-        } catch (Exception e){
-            throw new RuntimeException("获取文件失败,请检查配置信息", e);
-        }
-    }
-
-    public static boolean delete(String path) {
-     return Objects.requireNonNull(OssFactory.build()).delete(path);
-    }
-
-
-    public static String  uploadFileByte(String fileName, byte[] file) throws Exception {
-        if (file.length == 0) {
-            throw new RuntimeException("上传文件不能为空");
-        }
-        //上传文件
-        String suffix = Objects.requireNonNull(fileName).substring(fileName.lastIndexOf(StringPool.DOT));
-        return Objects.requireNonNull(OssFactory.build()).uploadSuffix(file, suffix);
-    }
-}
+package com.xjrsoft.common.utils;
+
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.xjrsoft.common.constant.GlobalConstant;
+import com.xjrsoft.common.exception.MyException;
+import com.xjrsoft.common.factory.OssFactory;
+import com.xjrsoft.config.FileCheckRuleConfig;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.ResponseBody;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class UploadUtil {
+
+    //单文件上传
+    public static String  uploadFile(MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            throw new RuntimeException("上传文件不能为空");
+        }
+        //上传文件
+        String suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf(StringPool.DOT));
+        return Objects.requireNonNull(OssFactory.build()).uploadSuffix(file.getBytes(), suffix);
+    }
+
+    //多文件上传
+    public  static List<String> uploadFiles(List<MultipartFile> files) throws Exception {
+        if (CollectionUtils.isNotEmpty(files)) {
+            throw new RuntimeException("上传文件不能为空");
+        }
+        List<String> urls = new ArrayList<>();
+        //上传文件
+        for (MultipartFile file : files) {
+            urls.add(uploadFile(file));
+        }
+        return urls;
+    }
+
+    public static InputStream download(String path) {
+        try {
+            OkHttpClient client = new OkHttpClient();
+            Request req = new Request.Builder().url(path).build();
+            InputStream inputStream = null;
+                okhttp3.Response resp = client.newCall(req).execute();
+                if (resp.isSuccessful()) {
+                    ResponseBody body = resp.body();
+                    inputStream = body.byteStream();
+                }
+            return inputStream;
+        } catch (Exception e){
+            throw new RuntimeException("获取文件失败,请检查配置信息", e);
+        }
+    }
+
+    public static boolean delete(String path) {
+     return Objects.requireNonNull(OssFactory.build()).delete(path);
+    }
+
+
+    public static String  uploadFileByte(String fileName, byte[] file) throws Exception {
+        if (file.length == 0) {
+            throw new RuntimeException("上传文件不能为空");
+        }
+        //上传文件
+        String suffix = Objects.requireNonNull(fileName).substring(fileName.lastIndexOf(StringPool.DOT));
+        return Objects.requireNonNull(OssFactory.build()).uploadSuffix(file, suffix);
+    }
+
+    /**
+     * 校验文件
+     * @param file 上传的文件
+     * @param rule 校验规则
+     * @throws MyException 校验失败抛出异常
+     */
+    public static void fileTypeValidate(MultipartFile file, FileCheckRuleConfig rule) {
+        // 2. 文件名校验
+        String originalFilename = file.getOriginalFilename();
+        if (StringUtils.isBlank(originalFilename)) {
+            throw new MyException("文件名不能为空");
+        }
+
+        // 3. 文件扩展名校验
+        String fileExtension = getFileExtension(originalFilename);
+        if (!rule.getAllowedExtensions().isEmpty() &&
+                !rule.getAllowedExtensions().contains(fileExtension.toLowerCase())) {
+            throw new MyException("不支持的文件类型,仅支持: " +
+                    String.join(", ", rule.getAllowedExtensions()));
+        }
+
+        // 4. MIME类型校验
+        String contentType = file.getContentType();
+        if (!rule.getAllowedMimeTypes().isEmpty() &&
+                !rule.getAllowedMimeTypes().contains(contentType)) {
+            throw new MyException("文件类型不匹配,仅支持: " +
+                    String.join(", ", rule.getAllowedMimeTypes()));
+        }
+    }
+
+    // 安全的获取扩展名方法
+    private static String getFileExtension(String filename) {
+        // 处理没有扩展名的情况
+        int dotIndex = filename.lastIndexOf('.');
+        if (dotIndex < 0) {
+            throw new MyException("文件缺少扩展名");
+        }
+        return filename.substring(dotIndex + 1).toLowerCase();
+    }
+}

+ 154 - 0
src/main/java/com/xjrsoft/config/FileCheckRuleConfig.java

@@ -0,0 +1,154 @@
+package com.xjrsoft.config;
+
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 文件校验规则配置(优化版)
+ */
+@Getter
+public final class FileCheckRuleConfig {
+    /* ========== 基础类型规则 ========== */
+    /**
+     * 图片规则(严格模式)
+     */
+    public static final FileCheckRuleConfig IMAGE = builder()
+            .withExtensions("jpg", "jpeg", "png", "gif", "webp", "bmp")
+            .withMimeTypes(
+                    "image/jpeg",
+                    "image/png",
+                    "image/gif",
+                    "image/webp",
+                    "image/bmp"
+            )
+            .build();
+    /**
+     * 视频规则
+     */
+    public static final FileCheckRuleConfig VIDEO = builder()
+            .withExtensions("mp4", "mov", "avi", "wmv", "flv", "mkv")
+            .withMimeTypes(
+                    "video/mp4",
+                    "video/quicktime",
+                    "video/x-msvideo",
+                    "video/x-ms-wmv",
+                    "video/x-flv",
+                    "video/x-matroska"
+            )
+            .build();
+    /**
+     * 音频规则
+     */
+    public static final FileCheckRuleConfig AUDIO = builder()
+            .withExtensions("mp3", "wav", "aac", "flac", "ogg", "m4a")
+            .withMimeTypes(
+                    "audio/mpeg",
+                    "audio/wav",
+                    "audio/aac",
+                    "audio/flac",
+                    "audio/ogg",
+                    "audio/x-m4a"
+            )
+            .build();
+    /**
+     * 文档规则
+     */
+    public static final FileCheckRuleConfig DOCUMENT = builder()
+            .withExtensions("pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt")
+            .withMimeTypes(
+                    "application/pdf",
+                    "application/msword",
+                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+                    "application/vnd.ms-excel",
+                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+                    "application/vnd.ms-powerpoint",
+                    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+                    "text/plain"
+            )
+            .build();
+    /* ========== 组合类型规则 ========== */
+    /**
+     * 媒体规则(图片+视频)
+     */
+    public static final FileCheckRuleConfig MEDIA = combine(IMAGE, VIDEO);
+    /**
+     * 图片和文档规则
+     */
+    public static final FileCheckRuleConfig IMAGE_AND_DOCUMENT = combine(IMAGE, DOCUMENT);
+    /**
+     * 所有类型规则
+     */
+    public static final FileCheckRuleConfig ALL = combine(IMAGE, VIDEO, AUDIO, DOCUMENT);
+
+    private final Set<String> allowedExtensions;
+    private final Set<String> allowedMimeTypes;
+
+    private FileCheckRuleConfig(Builder builder) {
+        this.allowedExtensions = Collections.unmodifiableSet(builder.extensions);
+        this.allowedMimeTypes = Collections.unmodifiableSet(builder.mimeTypes);
+    }
+
+    /**
+     * 创建新的构建器
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * 合并多个规则配置
+     */
+    public static FileCheckRuleConfig combine(FileCheckRuleConfig... configs) {
+        Builder builder = builder();
+        for (FileCheckRuleConfig config : configs) {
+            builder.extensions.addAll(config.allowedExtensions);
+            builder.mimeTypes.addAll(config.allowedMimeTypes);
+        }
+        return builder.build();
+    }
+
+    /**
+     * 构建器类
+     */
+    public static final class Builder {
+        private final Set<String> extensions = new HashSet<>();
+        private final Set<String> mimeTypes = new HashSet<>();
+
+        private Builder() {
+        }
+
+        public Builder withExtensions(String... extensions) {
+            this.extensions.addAll(Arrays.asList(extensions));
+            return this;
+        }
+
+        public Builder withMimeTypes(String... mimeTypes) {
+            this.mimeTypes.addAll(Arrays.asList(mimeTypes));
+            return this;
+        }
+
+        public Builder addExtension(String extension) {
+            this.extensions.add(extension);
+            return this;
+        }
+
+        public Builder addMimeType(String mimeType) {
+            this.mimeTypes.add(mimeType);
+            return this;
+        }
+
+        public Builder from(FileCheckRuleConfig config) {
+            this.extensions.addAll(config.allowedExtensions);
+            this.mimeTypes.addAll(config.allowedMimeTypes);
+            return this;
+        }
+
+        public FileCheckRuleConfig build() {
+            return new FileCheckRuleConfig(this);
+        }
+    }
+}

+ 5 - 0
src/main/java/com/xjrsoft/module/organization/controller/UserController.java

@@ -27,7 +27,9 @@ import com.xjrsoft.common.page.PageOutput;
 import com.xjrsoft.common.sms.SmsCtcc;
 import com.xjrsoft.common.utils.RedisUtil;
 import com.xjrsoft.common.utils.TreeUtil;
+import com.xjrsoft.common.utils.UploadUtil;
 import com.xjrsoft.common.utils.VoToColumnUtil;
+import com.xjrsoft.config.FileCheckRuleConfig;
 import com.xjrsoft.module.base.entity.BaseClass;
 import com.xjrsoft.module.base.entity.BaseGrade;
 import com.xjrsoft.module.base.entity.WhitelistManagement;
@@ -645,6 +647,9 @@ public class UserController {
             throw new MyException("上传文件不能为空");
         }
 
+        // 文件上传mime校验
+        UploadUtil.fileTypeValidate(file, FileCheckRuleConfig.IMAGE);
+
         //上传文件
         String suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf(StringPool.DOT));
         String url = Objects.requireNonNull(OssFactory.build()).uploadSuffix(file.getBytes(), suffix);

+ 8 - 0
src/main/java/com/xjrsoft/module/oss/controller/OssController.java

@@ -13,6 +13,8 @@ import com.xjrsoft.common.annotation.XjrLog;
 import com.xjrsoft.common.constant.GlobalConstant;
 import com.xjrsoft.common.exception.MyException;
 import com.xjrsoft.common.model.result.R;
+import com.xjrsoft.common.utils.UploadUtil;
+import com.xjrsoft.config.FileCheckRuleConfig;
 import com.xjrsoft.module.oss.factory.OssFactory;
 import com.xjrsoft.module.system.entity.File;
 import com.xjrsoft.module.system.service.IFileService;
@@ -58,6 +60,9 @@ public class OssController {
             throw new MyException("上传文件不能为空");
         }
 
+        // 文件上传mime校验
+        UploadUtil.fileTypeValidate(file, FileCheckRuleConfig.ALL);
+
         //上传文件
         String suffix = Objects.requireNonNull(file.getOriginalFilename()).substring(file.getOriginalFilename().lastIndexOf(StringPool.DOT));
         String url = Objects.requireNonNull(OssFactory.build()).uploadSuffix(file.getBytes(), suffix);
@@ -155,6 +160,9 @@ public class OssController {
         //上传文件
         List<FileVo> resultList = new ArrayList<>();
         for (MultipartFile f : file) {
+            // 文件上传mime校验
+            UploadUtil.fileTypeValidate(f, FileCheckRuleConfig.ALL);
+
             String suffix = Objects.requireNonNull(f.getOriginalFilename()).substring(f.getOriginalFilename().lastIndexOf(StringPool.DOT));
             String url = Objects.requireNonNull(OssFactory.build()).uploadSuffix(f.getBytes(), suffix);
 

+ 4 - 1
src/main/java/com/xjrsoft/module/system/controller/FileController.java

@@ -19,6 +19,7 @@ import com.xjrsoft.common.page.ConventPage;
 import com.xjrsoft.common.page.PageOutput;
 import com.xjrsoft.common.utils.UploadUtil;
 import com.xjrsoft.common.utils.VoToColumnUtil;
+import com.xjrsoft.config.FileCheckRuleConfig;
 import com.xjrsoft.config.OSSConfig;
 import com.xjrsoft.module.organization.dto.DownloadFileDto;
 import com.xjrsoft.module.organization.entity.User;
@@ -178,7 +179,6 @@ public class FileController {
                 File file = uploadFile(multipartFile, folderId, fileId);
                 urlList.add(file.getFileUrl());
             }
-
         }
         return R.ok(urlList);
     }
@@ -203,6 +203,9 @@ public class FileController {
     }
 
     private File uploadFile(MultipartFile file, Long folderId, Long fileId) throws Exception {
+        // 文件上传mime校验
+        UploadUtil.fileTypeValidate(file, FileCheckRuleConfig.ALL);
+
         String filename = file.getOriginalFilename();
         String suffix = StringUtils.substringAfterLast(filename, StringPool.DOT);
         //保存到云服务器

+ 6 - 0
src/main/java/com/xjrsoft/module/system/service/impl/FileServiceImpl.java

@@ -12,6 +12,8 @@ import com.xjrsoft.common.exception.MyException;
 import com.xjrsoft.common.utils.FileZipUtil;
 import com.xjrsoft.common.utils.ImageUtil;
 import com.xjrsoft.common.utils.PictureUtil;
+import com.xjrsoft.common.utils.UploadUtil;
+import com.xjrsoft.config.FileCheckRuleConfig;
 import com.xjrsoft.module.oss.factory.OssFactory;
 import com.xjrsoft.module.system.entity.File;
 import com.xjrsoft.module.system.mapper.FileMapper;
@@ -134,6 +136,10 @@ public class FileServiceImpl extends MPJBaseServiceImpl<FileMapper, File> implem
     @Override
     @Transactional(rollbackFor = Exception.class)
     public ImageVo addRubAndHand(MultipartFile file) {
+
+        // 文件上传mime校验
+        UploadUtil.fileTypeValidate(file, FileCheckRuleConfig.IMAGE);
+
         int anInt = 1;
         try {
             // 读取图片的元数据