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