From 17925215d37dd97d744c9296b185aeb16d3e44fb Mon Sep 17 00:00:00 2001
From: Ludc <2870569285@qq.com>
Date: 星期二, 18 十一月 2025 20:06:12 +0800
Subject: [PATCH] URL请求路径安全校验
---
Source/UBCS/ubcs-common/src/main/java/com/vci/ubcs/common/validator/ComprehensiveFileValidator.java | 477 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 477 insertions(+), 0 deletions(-)
diff --git a/Source/UBCS/ubcs-common/src/main/java/com/vci/ubcs/common/validator/ComprehensiveFileValidator.java b/Source/UBCS/ubcs-common/src/main/java/com/vci/ubcs/common/validator/ComprehensiveFileValidator.java
new file mode 100644
index 0000000..96b19cc
--- /dev/null
+++ b/Source/UBCS/ubcs-common/src/main/java/com/vci/ubcs/common/validator/ComprehensiveFileValidator.java
@@ -0,0 +1,477 @@
+package com.vci.ubcs.common.validator;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 鏂囦欢瀹夊叏楠岃瘉鍣�
+ */
+@Component
+@Slf4j
+public class ComprehensiveFileValidator {
+
+ /**
+ * 鏂囦欢鐧藉悕鍗�
+ */
+ @Value("${app.upload.security.allowed-extensions:jpg,jpeg,png,pdf}")
+ private String allowedExtensionsConfig;
+
+ /**
+ * 澶氶噸鎵╁睍鍚嶆枃浠剁姝�
+ */
+ @Value("${app.upload.security.prevent-multiple-extensions:true}")
+ private boolean preventMultipleExtensions;
+
+ /**
+ * 闄愬埗鐨勫嵄闄╂枃浠剁被鍨�
+ */
+ @Value("${app.upload.security.dangerous-primary-extensions:jsp,jspx,php,asp,aspx,war,exe,sh,bat}")
+ private String dangerousExtensionsConfig;
+
+ /**
+ * 鏂囦欢鍐呭绫诲瀷鏄惁鍖归厤鏍¢獙
+ */
+ @Value("${app.upload.security.validate-content-type:true}")
+ private boolean validateContentType;
+
+ /**
+ * 鏂囦欢澶撮獙璇�
+ */
+ @Value("${app.upload.security.validate-file-header:true}")
+ private boolean validateFileHeader;
+
+ /**
+ * 涓ユ牸妯″紡
+ */
+ @Value("${app.upload.security.strict-mode:false}")
+ private boolean strictMode;
+
+ /**
+ * 鍏佽涓婁紶鐨勫悗缂�
+ */
+ private Set<String> allowedExtensions;
+
+ /**
+ * 鍗遍櫓鐨勬枃浠跺悗缂�
+ */
+ private Set<String> dangerousPrimaryExtensions;
+
+ @PostConstruct
+ public void init() {
+ // 瑙f瀽閫楀彿鍒嗛殧鐨勯厤缃�
+ this.allowedExtensions = parseCommaSeparatedConfig(allowedExtensionsConfig);
+ this.dangerousPrimaryExtensions = parseCommaSeparatedConfig(dangerousExtensionsConfig);
+
+ log.info("鏂囦欢涓婁紶楠岃瘉鍣ㄥ垵濮嬪寲瀹屾垚");
+ log.info("鍏佽鐨勬墿灞曞悕: {}", allowedExtensions);
+ log.info("鍗遍櫓鎵╁睍鍚�: {}", dangerousPrimaryExtensions);
+ }
+
+ private Set<String> parseCommaSeparatedConfig(String config) {
+ if (config == null || config.trim().isEmpty()) {
+ return new HashSet<>();
+ }
+ return Arrays.stream(config.split(","))
+ .map(String::trim)
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet());
+ }
+
+ /**
+ * 楠岃瘉鍗曚釜鏂囦欢
+ */
+ public UploadValidationResult validateFile(MultipartFile file) {
+ UploadValidationResult result = new UploadValidationResult();
+
+ try {
+ // 鍩虹妫�鏌�
+ if (!basicValidation(file, result)) {
+ return result;
+ }
+
+ String filename = file.getOriginalFilename();
+
+ // 鏂囦欢鍚嶅畨鍏ㄩ獙璇�
+ if (!filenameSecurityValidation(filename, result)) {
+ return result;
+ }
+
+ // 鍐呭瀹夊叏楠岃瘉
+ if (!contentSecurityValidation(file, result)) {
+ return result;
+ }
+
+ result.setValid(true);
+ result.setMessage("鏂囦欢楠岃瘉閫氳繃");
+
+ } catch (Exception e) {
+ log.error("鏂囦欢楠岃瘉寮傚父", e);
+ result.setValid(false);
+ result.setMessage("楠岃瘉杩囩▼鍙戠敓寮傚父");
+ }
+
+ return result;
+ }
+
+ /**
+ * 楠岃瘉澶氫釜鏂囦欢
+ * @param files 鏂囦欢鍒楄〃
+ * @return 澶氫釜鏂囦欢鐨勯獙璇佺粨鏋�
+ */
+ public MultiUploadValidationResult validateFiles(List<MultipartFile> files) {
+ return validateFiles(files, false);
+ }
+
+ /**
+ * 楠岃瘉澶氫釜鏂囦欢
+ * @param files 鏂囦欢鍒楄〃
+ * @param stopOnFirstError 閬囧埌绗竴涓敊璇槸鍚﹀仠姝㈤獙璇�
+ * @return 澶氫釜鏂囦欢鐨勯獙璇佺粨鏋�
+ */
+ public MultiUploadValidationResult validateFiles(List<MultipartFile> files, boolean stopOnFirstError) {
+ MultiUploadValidationResult result = new MultiUploadValidationResult();
+
+ if (files == null || files.isEmpty()) {
+ result.setValid(false);
+ result.setMessage("鏂囦欢鍒楄〃涓虹┖");
+ return result;
+ }
+
+ List<FileValidationDetail> details = new ArrayList<>();
+ boolean allValid = true;
+
+ for (int i = 0; i < files.size(); i++) {
+ MultipartFile file = files.get(i);
+ FileValidationDetail detail = new FileValidationDetail();
+ detail.setFileName(file.getOriginalFilename());
+ detail.setFileIndex(i);
+ detail.setFileSize(file.getSize());
+
+ // 楠岃瘉鍗曚釜鏂囦欢
+ UploadValidationResult singleResult = validateFile(file);
+ detail.setValid(singleResult.isValid());
+ detail.setMessage(singleResult.getMessage());
+ detail.setDetectedType(singleResult.getDetectedType());
+
+ details.add(detail);
+
+ if (!singleResult.isValid()) {
+ allValid = false;
+ if (stopOnFirstError) {
+ // 閬囧埌閿欒涓旇缃负蹇�熷け璐ワ紝绔嬪嵆杩斿洖
+ result.setValid(false);
+ result.setMessage("绗�" + (i + 1) + "涓枃浠堕獙璇佸け璐�: " + file.getOriginalFilename());
+ result.setDetails(details);
+ result.setFailedIndex(i);
+ return result;
+ }
+ }
+ }
+
+ result.setValid(allValid);
+ result.setMessage(allValid ? "鎵�鏈夋枃浠堕獙璇侀�氳繃" : "閮ㄥ垎鏂囦欢楠岃瘉澶辫触");
+ result.setDetails(details);
+ result.setTotalFiles(files.size());
+ result.setValidFiles((int) details.stream().filter(FileValidationDetail::isValid).count());
+ result.setInvalidFiles((int) details.stream().filter(d -> !d.isValid()).count());
+
+ return result;
+ }
+
+ /**
+ * 楠岃瘉澶氫釜鏂囦欢锛堟暟缁勭増鏈級
+ */
+ public MultiUploadValidationResult validateFiles(MultipartFile[] files) {
+ return validateFiles(Arrays.asList(files));
+ }
+
+ /**
+ * 楠岃瘉澶氫釜鏂囦欢锛堟暟缁勭増鏈紝鍙缃槸鍚﹀揩閫熷け璐ワ級
+ */
+ public MultiUploadValidationResult validateFiles(MultipartFile[] files, boolean stopOnFirstError) {
+ return validateFiles(Arrays.asList(files), stopOnFirstError);
+ }
+
+ /**
+ * 鎵归噺楠岃瘉鏂囦欢骞惰繑鍥炴湁鏁堢殑鏂囦欢鍒楄〃
+ */
+ public List<MultipartFile> getValidFiles(List<MultipartFile> files) {
+ MultiUploadValidationResult result = validateFiles(files);
+ List<MultipartFile> validFiles = new ArrayList<>();
+
+ for (int i = 0; i < files.size(); i++) {
+ if (result.getDetails().get(i).isValid()) {
+ validFiles.add(files.get(i));
+ }
+ }
+
+ return validFiles;
+ }
+
+ /**
+ * 妫�鏌ユ槸鍚︽墍鏈夋枃浠堕兘鏈夋晥
+ */
+ public boolean areAllFilesValid(List<MultipartFile> files) {
+ MultiUploadValidationResult result = validateFiles(files);
+ return result.isValid();
+ }
+
+ // 鍘熸湁鐨勭鏈夋柟娉曚繚鎸佷笉鍙�
+ private boolean basicValidation(MultipartFile file, UploadValidationResult result) {
+ if (file == null || file.isEmpty()) {
+ result.setMessage("鏂囦欢涓虹┖");
+ return false;
+ }
+
+ String filename = file.getOriginalFilename();
+ if (filename == null || filename.trim().isEmpty()) {
+ result.setMessage("鏂囦欢鍚嶄负绌�");
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean filenameSecurityValidation(String filename, UploadValidationResult result) {
+ // 璺緞閬嶅巻妫�鏌�
+ if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
+ result.setMessage("鏂囦欢鍚嶅寘鍚嵄闄╁瓧绗�");
+ return false;
+ }
+
+ // 鎵╁睍鍚嶆鏌�
+ String finalExtension = getFinalExtension(filename);
+ if (finalExtension.isEmpty() || !allowedExtensions.contains(finalExtension.toLowerCase())) {
+ result.setMessage("涓嶆敮鎸佺殑鏂囦欢绫诲瀷: " + finalExtension);
+ return false;
+ }
+
+ // 澶氶噸鎵╁睍鍚嶆鏌�
+ if (preventMultipleExtensions && hasMultipleExtensions(filename)) {
+ if (strictMode) {
+ // 涓ユ牸妯″紡锛氭嫤鎴墍鏈夊閲嶆墿灞曞悕
+ result.setMessage("澶氶噸鎵╁睍鍚嶆枃浠惰绂佹");
+ return false;
+ } else {
+ // 鏅�氭ā寮忥細鍙嫤鎴寘鍚嵄闄╂墿灞曞悕鐨勫閲嶆墿灞曞悕
+ if (containsDangerousExtension(filename)) {
+ result.setMessage("妫�娴嬪埌浼Webshell鏂囦欢: " + filename);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean contentSecurityValidation(MultipartFile file, UploadValidationResult result) {
+ // 鍐呭绫诲瀷楠岃瘉
+ if (validateContentType && !validateContentType(file)) {
+ result.setMessage("鏂囦欢鍐呭绫诲瀷涓嶅尮閰�");
+ return false;
+ }
+
+ // 鏂囦欢澶撮獙璇�
+ if (validateFileHeader && !validateFileHeader(file)) {
+ result.setMessage("鏂囦欢澶撮獙璇佸け璐�");
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean hasMultipleExtensions(String filename) {
+ String name = getFileNameWithoutPath(filename);
+ return name.chars().filter(ch -> ch == '.').count() > 1;
+ }
+
+ private boolean containsDangerousExtension(String filename) {
+ String name = getFileNameWithoutPath(filename);
+ String[] parts = name.split("\\.");
+
+ // 妫�鏌ラ櫎鏈�鍚庝竴涓墿灞曞悕涔嬪鐨勬墍鏈夐儴鍒�
+ for (int i = 0; i < parts.length - 1; i++) {
+ String part = parts[i].toLowerCase();
+ if (dangerousPrimaryExtensions.contains(part)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean validateContentType(MultipartFile file) {
+ try {
+ String declaredType = file.getContentType();
+ if (declaredType == null) {
+ return true; // 娌℃湁澹版槑绫诲瀷锛屾斁杩�
+ }
+
+ // 绠�鍗曠殑绫诲瀷鍖归厤妫�鏌�
+ String finalExtension = getFinalExtension(file.getOriginalFilename()).toLowerCase();
+ return isContentTypeConsistent(declaredType, finalExtension);
+ } catch (Exception e) {
+ log.error("鍐呭绫诲瀷楠岃瘉澶辫触", e);
+ return false;
+ }
+ }
+
+ /**
+ * 楠岃瘉鏂囦欢鐨勫唴瀹圭被鍨嬶紙Content-Type锛夋槸鍚︿笌鏂囦欢鎵╁睍鍚嶄竴鑷�
+ * @param contentType
+ * @param extension
+ * @return
+ */
+ private boolean isContentTypeConsistent(String contentType, String extension) {
+ // 鎵╁睍鏇村叏闈㈢殑绫诲瀷鏄犲皠
+ Map<String, String> expectedTypes = new HashMap<>();
+
+ // 鍥剧墖绫诲瀷
+ expectedTypes.put("jpg", "image/jpeg");
+ expectedTypes.put("jpeg", "image/jpeg");
+ expectedTypes.put("png", "image/png");
+ expectedTypes.put("gif", "image/gif");
+ expectedTypes.put("bmp", "image/bmp");
+ expectedTypes.put("webp", "image/webp");
+ expectedTypes.put("svg", "image/svg+xml");
+
+ // 鏂囨。绫诲瀷
+ expectedTypes.put("pdf", "application/pdf");
+ expectedTypes.put("doc", "application/msword");
+ expectedTypes.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+ expectedTypes.put("xls", "application/vnd.ms-excel");
+ expectedTypes.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ expectedTypes.put("ppt", "application/vnd.ms-powerpoint");
+ expectedTypes.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
+ expectedTypes.put("txt", "text/plain");
+
+ // 鍘嬬缉鏂囦欢
+ expectedTypes.put("zip", "application/zip");
+ expectedTypes.put("rar", "application/x-rar-compressed");
+ expectedTypes.put("7z", "application/x-7z-compressed");
+
+ String expectedType = expectedTypes.get(extension);
+ return expectedType == null || expectedType.equalsIgnoreCase(contentType);
+ }
+
+ private boolean validateFileHeader(MultipartFile file) {
+ try {
+ byte[] header = new byte[8];
+ int bytesRead = file.getInputStream().read(header);
+
+ if (bytesRead < 4) {
+ return false;
+ }
+
+ String finalExtension = getFinalExtension(file.getOriginalFilename()).toLowerCase();
+
+ // 鍩虹鐨勬枃浠跺ご楠岃瘉
+ switch (finalExtension) {
+ case "jpg":
+ case "jpeg":
+ return isJpeg(header);
+ case "png":
+ return isPng(header);
+ case "pdf":
+ return isPdf(header);
+ case "gif":
+ return isGif(header);
+ default:
+ return true; // 鍏朵粬绫诲瀷涓嶉獙璇佹枃浠跺ご
+ }
+ } catch (IOException e) {
+ log.error("鏂囦欢澶撮獙璇佸け璐�", e);
+ return false;
+ }
+ }
+
+ /**
+ * 鏂囦欢澶撮獙璇佹柟娉�
+ * @param header
+ * @return
+ */
+ private boolean isJpeg(byte[] header) {
+ return (header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8;
+ }
+
+ private boolean isPng(byte[] header) {
+ return header[0] == (byte) 0x89 && header[1] == 0x50 &&
+ header[2] == 0x4E && header[3] == 0x47;
+ }
+
+ private boolean isPdf(byte[] header) {
+ return header[0] == 0x25 && header[1] == 0x50 &&
+ header[2] == 0x44 && header[3] == 0x46;
+ }
+
+ private boolean isGif(byte[] header) {
+ return header[0] == 'G' && header[1] == 'I' &&
+ header[2] == 'F' && header[3] == '8';
+ }
+
+ // 杈呭姪鏂规硶
+ private String getFinalExtension(String filename) {
+ if (filename == null || !filename.contains(".")) return "";
+ String[] parts = filename.split("\\.");
+ return parts[parts.length - 1];
+ }
+
+ private String getFileNameWithoutPath(String filename) {
+ if (filename == null) return "";
+ filename = filename.replace('\\', '/');
+ int lastSlash = filename.lastIndexOf('/');
+ return lastSlash >= 0 ? filename.substring(lastSlash + 1) : filename;
+ }
+
+ @Data
+ public static class UploadValidationResult {
+ private boolean valid;
+ private String message;
+ private String detectedType;
+
+ public UploadValidationResult() {
+ this.valid = false;
+ this.message = "";
+ }
+ }
+
+ /**
+ * 澶氭枃浠堕獙璇佺粨鏋�
+ */
+ @Data
+ public static class MultiUploadValidationResult {
+ private boolean valid;
+ private String message;
+ private int totalFiles;
+ private int validFiles;
+ private int invalidFiles;
+ private int failedIndex = -1; // 绗竴涓け璐ョ殑鏂囦欢绱㈠紩
+ private List<FileValidationDetail> details;
+
+ public MultiUploadValidationResult() {
+ this.valid = false;
+ this.message = "";
+ this.details = new ArrayList<>();
+ }
+ }
+
+ /**
+ * 鍗曚釜鏂囦欢楠岃瘉璇︽儏
+ */
+ @Data
+ public static class FileValidationDetail {
+ private String fileName;
+ private int fileIndex;
+ private long fileSize;
+ private boolean valid;
+ private String message;
+ private String detectedType;
+ }
+}
--
Gitblit v1.9.3