|
|
@@ -0,0 +1,352 @@
|
|
|
+package org.springblade.archive.utils;
|
|
|
+import java.io.*;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.*;
|
|
|
+import java.util.zip.ZipEntry;
|
|
|
+import java.util.zip.ZipFile;
|
|
|
+
|
|
|
+public class FileFormatSecurityChecker {
|
|
|
+
|
|
|
+ // PDF相关常量
|
|
|
+ private static final byte[] PDF_HEADER = "%PDF".getBytes();
|
|
|
+ private static final String[] PDF_STANDARD_FILTERS = {
|
|
|
+ "FlateDecode", "LZWDecode", "ASCII85Decode", "ASCIIHexDecode",
|
|
|
+ "RunLengthDecode", "CCITTFaxDecode", "DCTDecode", "JBIG2Decode"
|
|
|
+ };
|
|
|
+
|
|
|
+ public static FileAnalysisResult analyzeFile(String filePath) throws IOException {
|
|
|
+ File file = new File(filePath);
|
|
|
+ if (!file.exists()) {
|
|
|
+ throw new FileNotFoundException("文件不存在: " + filePath);
|
|
|
+ }
|
|
|
+
|
|
|
+ String filename = file.getName().toLowerCase();
|
|
|
+ FileAnalysisResult result = new FileAnalysisResult(filePath);
|
|
|
+
|
|
|
+ if (filename.endsWith(".pdf")) {
|
|
|
+ return analyzePdf(file, result);
|
|
|
+ } else if (filename.endsWith(".xlsx")) {
|
|
|
+ return analyzeXlsx(file, result);
|
|
|
+ } else if (filename.endsWith(".png")) {
|
|
|
+ return analyzePng(file, result);
|
|
|
+ } else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
|
|
|
+ return analyzeJpeg(file, result);
|
|
|
+ } else {
|
|
|
+ result.addIssue("不支持的文件格式");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // PDF文件分析
|
|
|
+ private static FileAnalysisResult analyzePdf(File file, FileAnalysisResult result) throws IOException {
|
|
|
+ result.setFormat("PDF");
|
|
|
+
|
|
|
+ byte[] fileContent = Files.readAllBytes(file.toPath());
|
|
|
+ String content = new String(fileContent, "ISO-8859-1");
|
|
|
+
|
|
|
+ // 1. 检查加密
|
|
|
+ boolean isEncrypted = checkPdfEncryption(content);
|
|
|
+ result.setEncrypted(isEncrypted);
|
|
|
+
|
|
|
+ // 2. 检查压缩算法
|
|
|
+ CompressionAnalysis compressionAnalysis = checkPdfCompression(content);
|
|
|
+ result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
|
|
|
+ result.setStandardCompression(compressionAnalysis.isStandard());
|
|
|
+ result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
|
|
|
+
|
|
|
+ // 3. 检查其他安全问题
|
|
|
+ checkPdfSecurity(content, result);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean checkPdfEncryption(String pdfContent) {
|
|
|
+ // 检查加密字典
|
|
|
+ return pdfContent.contains("/Encrypt") ||
|
|
|
+ pdfContent.contains("/Filter/Standard") ||
|
|
|
+ pdfContent.contains("/CFM/") ||
|
|
|
+ pdfContent.contains("/StmF/") ||
|
|
|
+ pdfContent.contains("/StrF/");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CompressionAnalysis checkPdfCompression(String pdfContent) {
|
|
|
+ CompressionAnalysis analysis = new CompressionAnalysis();
|
|
|
+ Set<String> foundAlgorithms = new HashSet<>();
|
|
|
+ Set<String> nonStandardAlgorithms = new HashSet<>();
|
|
|
+
|
|
|
+ // 查找所有使用的压缩过滤器
|
|
|
+ String[] lines = pdfContent.split("\n");
|
|
|
+ for (String line : lines) {
|
|
|
+ if (line.contains("/Filter")) {
|
|
|
+ // 提取过滤器名称
|
|
|
+ for (String filter : PDF_STANDARD_FILTERS) {
|
|
|
+ if (line.contains(filter)) {
|
|
|
+ foundAlgorithms.add(filter);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查非标准过滤器
|
|
|
+ if (line.contains("/Filter") && !containsStandardFilter(line)) {
|
|
|
+ // 提取可能的非标准过滤器名称
|
|
|
+ String[] parts = line.split("/");
|
|
|
+ for (String part : parts) {
|
|
|
+ if (part.trim().length() > 3 && !part.trim().equals("Filter")) {
|
|
|
+ boolean isStandard = false;
|
|
|
+ for (String stdFilter : PDF_STANDARD_FILTERS) {
|
|
|
+ if (part.contains(stdFilter.replace("Decode", ""))) {
|
|
|
+ isStandard = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!isStandard) {
|
|
|
+ nonStandardAlgorithms.add(part.trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置结果
|
|
|
+ if (!foundAlgorithms.isEmpty()) {
|
|
|
+ analysis.setAlgorithm(String.join(", ", foundAlgorithms));
|
|
|
+ } else {
|
|
|
+ analysis.setAlgorithm("未压缩或未知压缩");
|
|
|
+ }
|
|
|
+
|
|
|
+ analysis.setStandard(nonStandardAlgorithms.isEmpty());
|
|
|
+ analysis.setNonStandardAlgorithms(nonStandardAlgorithms);
|
|
|
+
|
|
|
+ return analysis;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean containsStandardFilter(String line) {
|
|
|
+ for (String filter : PDF_STANDARD_FILTERS) {
|
|
|
+ if (line.contains(filter)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void checkPdfSecurity(String pdfContent, FileAnalysisResult result) {
|
|
|
+ // 检查JavaScript(可能的安全风险)
|
|
|
+ if (pdfContent.contains("/JavaScript") || pdfContent.contains("/JS")) {
|
|
|
+ result.addIssue("包含JavaScript代码");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查外部引用
|
|
|
+ if (pdfContent.contains("/Launch") || pdfContent.contains("/URI")) {
|
|
|
+ result.addIssue("包含外部引用或启动动作");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // XLSX文件分析
|
|
|
+ private static FileAnalysisResult analyzeXlsx(File file, FileAnalysisResult result) throws IOException {
|
|
|
+ result.setFormat("XLSX");
|
|
|
+
|
|
|
+ try (ZipFile zipFile = new ZipFile(file)) {
|
|
|
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
|
+
|
|
|
+ // 1. 检查加密
|
|
|
+ boolean isEncrypted = checkXlsxEncryption(zipFile);
|
|
|
+ result.setEncrypted(isEncrypted);
|
|
|
+
|
|
|
+ // 2. 检查压缩算法
|
|
|
+ CompressionAnalysis compressionAnalysis = checkXlsxCompression(zipFile);
|
|
|
+ result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
|
|
|
+ result.setStandardCompression(compressionAnalysis.isStandard());
|
|
|
+ result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
|
|
|
+
|
|
|
+ // 3. 检查其他安全问题
|
|
|
+ checkXlsxSecurity(zipFile, result);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean checkXlsxEncryption(ZipFile zipFile) {
|
|
|
+ return zipFile.getEntry("EncryptedPackage") != null ||
|
|
|
+ zipFile.getEntry("EncryptionInfo") != null ||
|
|
|
+ zipFile.getEntry("encryption") != null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CompressionAnalysis checkXlsxCompression(ZipFile zipFile) {
|
|
|
+ CompressionAnalysis analysis = new CompressionAnalysis();
|
|
|
+ Set<String> nonStandardAlgorithms = new HashSet<>();
|
|
|
+
|
|
|
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
|
+ while (entries.hasMoreElements()) {
|
|
|
+ ZipEntry entry = entries.nextElement();
|
|
|
+ int method = entry.getMethod();
|
|
|
+
|
|
|
+ // ZIP标准压缩方法
|
|
|
+ if (method != ZipEntry.STORED && method != ZipEntry.DEFLATED) {
|
|
|
+ nonStandardAlgorithms.add("ZIP压缩方法: " + method);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ analysis.setAlgorithm("DEFLATE (标准ZIP压缩)");
|
|
|
+ analysis.setStandard(nonStandardAlgorithms.isEmpty());
|
|
|
+ analysis.setNonStandardAlgorithms(nonStandardAlgorithms);
|
|
|
+
|
|
|
+ return analysis;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void checkXlsxSecurity(ZipFile zipFile, FileAnalysisResult result) {
|
|
|
+ // 检查宏
|
|
|
+ if (zipFile.getEntry("xl/vbaProject.bin") != null) {
|
|
|
+ result.addIssue("包含VBA宏");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查外部链接
|
|
|
+ try {
|
|
|
+ ZipEntry externalLinks = zipFile.getEntry("xl/externalLinks/");
|
|
|
+ if (externalLinks != null) {
|
|
|
+ result.addIssue("包含外部链接");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 忽略
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // PNG文件分析
|
|
|
+ private static FileAnalysisResult analyzePng(File file, FileAnalysisResult result) throws IOException {
|
|
|
+ result.setFormat("PNG");
|
|
|
+ result.setEncrypted(false); // PNG不支持加密
|
|
|
+
|
|
|
+ byte[] data = Files.readAllBytes(file.toPath());
|
|
|
+
|
|
|
+ // 1. 检查文件头
|
|
|
+ if (!isValidPngHeader(data)) {
|
|
|
+ result.addIssue("无效的PNG文件头");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查压缩算法
|
|
|
+ CompressionAnalysis compressionAnalysis = checkPngCompression(data);
|
|
|
+ result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
|
|
|
+ result.setStandardCompression(compressionAnalysis.isStandard());
|
|
|
+ result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isValidPngHeader(byte[] data) {
|
|
|
+ // PNG文件头: 89 50 4E 47 0D 0A 1A 0A
|
|
|
+ return data.length >= 8 &&
|
|
|
+ data[0] == (byte)0x89 &&
|
|
|
+ data[1] == 0x50 &&
|
|
|
+ data[2] == 0x4E &&
|
|
|
+ data[3] == 0x47 &&
|
|
|
+ data[4] == 0x0D &&
|
|
|
+ data[5] == 0x0A &&
|
|
|
+ data[6] == 0x1A &&
|
|
|
+ data[7] == 0x0A;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CompressionAnalysis checkPngCompression(byte[] data) {
|
|
|
+ CompressionAnalysis analysis = new CompressionAnalysis();
|
|
|
+
|
|
|
+ // 在IHDR块后查找压缩方法
|
|
|
+ // PNG压缩方法位于第12个字节(从0开始),但需要找到IDAT块
|
|
|
+ for (int i = 8; i < data.length - 4; i++) {
|
|
|
+ // 查找IDAT块
|
|
|
+ if (data[i] == 'I' && data[i+1] == 'D' && data[i+2] == 'A' && data[i+3] == 'T') {
|
|
|
+ // IDAT块中的压缩方法通常是DEFLATE
|
|
|
+ // PNG标准只允许使用DEFLATE压缩
|
|
|
+ analysis.setAlgorithm("DEFLATE");
|
|
|
+ analysis.setStandard(true);
|
|
|
+ return analysis;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找不到IDAT块,检查是否有非标准块
|
|
|
+ analysis.setAlgorithm("未知");
|
|
|
+ analysis.setStandard(false);
|
|
|
+ analysis.addNonStandardAlgorithm("无法识别标准压缩块");
|
|
|
+
|
|
|
+ return analysis;
|
|
|
+ }
|
|
|
+
|
|
|
+ // JPEG文件分析
|
|
|
+ private static FileAnalysisResult analyzeJpeg(File file, FileAnalysisResult result) throws IOException {
|
|
|
+ result.setFormat("JPEG");
|
|
|
+ result.setEncrypted(false); // JPEG不支持加密
|
|
|
+
|
|
|
+ byte[] data = Files.readAllBytes(file.toPath());
|
|
|
+
|
|
|
+ // 1. 检查文件头
|
|
|
+ if (!isValidJpegHeader(data)) {
|
|
|
+ result.addIssue("无效的JPEG文件头");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查压缩算法
|
|
|
+ CompressionAnalysis compressionAnalysis = checkJpegCompression(data);
|
|
|
+ result.setCompressionAlgorithm(compressionAnalysis.getAlgorithm());
|
|
|
+ result.setStandardCompression(compressionAnalysis.isStandard());
|
|
|
+ result.setNonStandardAlgorithms(compressionAnalysis.getNonStandardAlgorithms());
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static boolean isValidJpegHeader(byte[] data) {
|
|
|
+ // JPEG文件头: FF D8
|
|
|
+ return data.length >= 2 &&
|
|
|
+ data[0] == (byte)0xFF &&
|
|
|
+ data[1] == (byte)0xD8;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static CompressionAnalysis checkJpegCompression(byte[] data) {
|
|
|
+ CompressionAnalysis analysis = new CompressionAnalysis();
|
|
|
+
|
|
|
+ // JPEG使用标准的DCT和Huffman编码
|
|
|
+ // 检查是否有非标准标记
|
|
|
+ Set<String> nonStandardAlgorithms = new HashSet<>();
|
|
|
+
|
|
|
+ for (int i = 0; i < data.length - 1; i++) {
|
|
|
+ if (data[i] == (byte)0xFF) {
|
|
|
+ int marker = data[i + 1] & 0xFF;
|
|
|
+
|
|
|
+ // 检查非标准或保留的标记
|
|
|
+ if (marker >= 0x02 && marker <= 0xBF && marker != 0x00) {
|
|
|
+ // 这些是标准标记范围,但某些特定值可能表示非标准扩展
|
|
|
+ if (marker == 0xFF || marker == 0x00) {
|
|
|
+ // 无效标记
|
|
|
+ nonStandardAlgorithms.add("无效JPEG标记: 0x" + Integer.toHexString(marker));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ analysis.setAlgorithm("DCT + Huffman (标准JPEG压缩)");
|
|
|
+ analysis.setStandard(nonStandardAlgorithms.isEmpty());
|
|
|
+ analysis.setNonStandardAlgorithms(nonStandardAlgorithms);
|
|
|
+
|
|
|
+ return analysis;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 压缩分析结果类
|
|
|
+class CompressionAnalysis {
|
|
|
+ private String algorithm;
|
|
|
+ private boolean isStandard;
|
|
|
+ private Set<String> nonStandardAlgorithms = new HashSet<>();
|
|
|
+
|
|
|
+ // getter和setter方法
|
|
|
+ public String getAlgorithm() { return algorithm; }
|
|
|
+ public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
|
|
|
+
|
|
|
+ public boolean isStandard() { return isStandard; }
|
|
|
+ public void setStandard(boolean standard) { isStandard = standard; }
|
|
|
+
|
|
|
+ public Set<String> getNonStandardAlgorithms() { return nonStandardAlgorithms; }
|
|
|
+ public void setNonStandardAlgorithms(Set<String> nonStandardAlgorithms) {
|
|
|
+ this.nonStandardAlgorithms = nonStandardAlgorithms;
|
|
|
+ }
|
|
|
+ public void addNonStandardAlgorithm(String algorithm) {
|
|
|
+ this.nonStandardAlgorithms.add(algorithm);
|
|
|
+ }
|
|
|
+}
|