10-java实现对上传文件做安全性检查

对外接口支持文件上传功能时,为避免有人恶意上传有毒或者篡改程序的脚本,需要对上传的文件添加安全性校验。

文章目录

  • 1.文件后缀校验
  • 2.校验文件头
    • 1.使用枚举类去校验
      • 1.魔数枚举类
      • 2.获取文件头,并校验是否为Excel
      • 2.FileMagic校验文件头
      • 3.校验文件大小
      • 4.示例

        1.文件后缀校验

        文件首先校验直观文件上传格式,校验文件后缀是否符合业务要求。以MultipartFile类为例

         String fileName = file.getOriginalFilename();
                if (Strings.isEmpty(fileName)) { throw new RuntimeException("文件名未找到");
                }
                String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
                if (!Objects.equal(suffix, "xls") && !Objects.equal(suffix, "xlsx")) { throw new RuntimeException("文件类型不正确,需要为xls或者xlsx");
                }
        

        2.校验文件头

        由于文件后缀可能涉及到篡改的情况出现,因此需要校验文件的魔数,也就是文件头。无论这个文件是否修改文件后缀,这个文件的文件头是不会改变的。下面是常用的文件头格式:

        1.使用枚举类去校验

        JPEG (jpg),文件头:FFD8FFE1
        PNG (png),文件头:89504E47
        GIF (gif),文件头:47494638
        TIFF (tif),文件头:49492A00
        Windows Bitmap (bmp),文件头:424D
        CAD (dwg),文件头:41433130
        Adobe Photoshop (psd),文件头:38425053
        Rich Text Format (rtf),文件头:7B5C727466
        XML (xml),文件头:3C3F786D6C
        HTML (html),文件头:68746D6C3E
        Email [thorough only] (eml),文件头:44656C69766572792D646174653A
        Outlook Express (dbx),文件头:CFAD12FEC5FD746F
        Outlook (pst),文件头:2142444E
        MS Word/Excel (xls.or.doc),文件头:D0CF11E0
        MS Access (mdb),文件头:5374616E64617264204A
        WordPerfect (wpd),文件头:FF575043
        Postscript (eps.or.ps),文件头:252150532D41646F6265
        Adobe Acrobat (pdf),文件头:255044462D312E
        Quicken (qdf),文件头:AC9EBD8F
        Windows Password (pwl),文件头:E3828596
        ZIP Archive (zip),文件头:504B0304
        RAR Archive (rar),文件头:52617221
        Wave (wav),文件头:57415645
        AVI (avi),文件头:41564920
        Real Audio (ram),文件头:2E7261FD
        Real Media (rm),文件头:2E524D46
        MPEG (mpg),文件头:000001BA
        MPEG (mpg),文件头:000001B3
        Quicktime (mov),文件头:6D6F6F76
        Windows Media (asf),文件头:3026B2758E66CF11
        MIDI (mid),文件头:4D546864
        

        使用上面的文件头去校验的代码示例(这段代码参考:Java 实战系列·Magic 魔数获取文件类型):

        1.魔数枚举类

        public enum FileType { /**
             * JPEG
             */
            JPEG("JPEG", "FFD8FF"),
            /**
             * PNG
             */
            PNG("PNG", "89504E47"),
            /**
             * GIF
             */
            GIF("GIF", "47494638"),
            /**
             * TIFF
             */
            TIFF("TIFF", "49492A00"),
            /**
             * Windows bitmap
             */
            BMP("BMP", "424D"),
            /**
             * CAD
             */
            DWG("DWG", "41433130"),
            /**
             * Adobe photoshop
             */
            PSD("PSD", "38425053"),
            /**
             * Rich Text Format
             */
            RTF("RTF", "7B5C727466"),
            /**
             * XML
             */
            XML("XML", "3C3F786D6C"),
            /**
             * HTML
             */
            HTML("HTML", "68746D6C3E"),
            /**
             * Outlook Express
             */
            DBX("DBX", "CFAD12FEC5FD746F "),
            /**
             * Outlook
             */
            PST("PST", "2142444E"),
            /**
             * doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db
             */
            OLE2("OLE2", "0xD0CF11E0A1B11AE1"),
            /**
             * Microsoft Word/Excel
             */
            XLS_DOC("XLS_DOC", "D0CF11E0"),
            /**
             * Microsoft Access
             */
            MDB("MDB", "5374616E64617264204A"),
            /**
             * Word Perfect
             */
            WPB("WPB", "FF575043"),
            /**
             * Postscript
             */
            EPS_PS("EPS_PS", "252150532D41646F6265"),
            /**
             * Adobe Acrobat
             */
            PDF("PDF", "255044462D312E"),
            /**
             * Windows Password
             */
            PWL("PWL", "E3828596"),
            /**
             * ZIP Archive
             */
            ZIP("ZIP", "504B0304"),
            /**
             * ARAR Archive
             */
            RAR("RAR", "52617221"),
            /**
             * WAVE
             */
            WAV("WAV", "57415645"),
            /**
             * AVI
             */
            AVI("AVI", "41564920"),
            /**
             * Real Audio
             */
            RAM("RAM", "2E7261FD"),
            /**
             * Real Media
             */
            RM("RM", "2E524D46"),
            /**
             * Quicktime
             */
            MOV("MOV", "6D6F6F76"),
            /**
             * Windows Media
             */
            ASF("ASF", "3026B2758E66CF11"),
            /**
             * MIDI
             */
            MID("MID", "4D546864");
            private String key;
            private String value;
            FileType(String key, String value) { this.key = key;
                this.value = value;
            }
            public String getValue() { return value;
            }
            public String getKey() { return key;
            }
        }
        

        2.获取文件头,并校验是否为Excel

        public class FileUtil { /**
             * 获取文件投
             *
             * @param filePath 文件路径
             * @return 16 进制的文件投信息
             *
             * @throws IOException
             */
            private static String getFileHeader(String filePath) throws IOException { byte[] b = new byte[28];
                InputStream inputStream = new FileInputStream(filePath);
                inputStream.read(b, 0, 28);
                inputStream.close();
                return bytes2hex(b);
            }
            /**
             * 将字节数组转换成16进制字符串
             */
            private static String bytes2hex(byte[] src) { StringBuilder stringBuilder = new StringBuilder("");
                if (src == null || src.length <= 0) { return null;
                }
                for (byte b : src) { int v = b & 0xFF;
                    String hv = Integer.toHexString(v);
                    if (hv.length() < 2) { stringBuilder.append(0);
                    }
                    stringBuilder.append(hv);
                }
                return stringBuilder.toString();
            }
            /**
             * 校验是否为excel
             *
             * @param filePath 文件路径
             * @return 文件类型
             *
             * @throws IOException
             */
            public static boolean checkIsExcel(String filePath) throws IOException { String fileHead = getFileHeader(filePath);
                if (null == fileHead || fileHead.length() == 0) { return false;
                }
                //校验是否为xls或者xlsx文件
                if (Objects.equal(fileHead, FileType.OLE2.getValue()) || Objects.equal(fileHead, FileType.XLS_DOC.getValue())) { return true;
                }
                return false;
            }
        }
        

        除了用上面的魔数头去校验,也可以用poi提供的枚举类FileMagic工具类去校验:

        2.FileMagic校验文件头

        FileMagic魔数值解释:

         OLE2(-2226271756974174256L),   //xls
            OOXML(new int[]{80, 75, 3, 4}), //xlsx, OOXML全称是Office Open XML,OOXML是由微软公司为Office 2007产品开发的技术规范,现已成为国际文档格式标准,兼容前国际标准ODF(Open Document Format)和中国文档标准UOF(Unified Office document Format)。
            XML(new int[]{60, 63, 120, 109, 108}),  //xml
            BIFF2(new int[]{9, 0, 4, 0, 0, 0, 63, 0}), //Excel 2 现在office已经不支持
            BIFF3(new int[]{9, 2, 6, 0, 0, 0, 63, 0}),//Excel 3现在office已经不支持
            BIFF4(new byte[][]{{9, 4, 6, 0, 0, 0, 63, 0}, {9, 4, 6, 0, 0, 0, 0, 1}}),//Excel 4 现在office已经不支持
            MSWRITE(new byte[][]{{49, -66, 0, 0}, {50, -66, 0, 0}}),  //微软原来的写入流,这个不清楚是否还能使用。
            RTF(new String[]{"{\\rtf"}),  //rtf
            PDF(new String[]{"%PDF"}), //pdf
            HTML(new String[]{"219, 165, 45, 0}),//word
            JPEG(new byte[][]{{-1, -40, -1, -37}, {-1, -40, -1, -32, 63, 63, 74, 70, 73, 70, 0, 1}, {-1, -40, -1, -18}, {-1, -40, -1, -31, 63, 63, 69, 120, 105, 102, 0, 0}}),//图片验证,jpeg格式
            GIF(new String[]{"GIF87a", "GIF89a"}),//图片验证,gif格式
            PNG(new int[]{137, 80, 78, 71, 13, 10, 26, 10}),//图片验证,png格式
            TIFF(new String[]{"II*\u0000", "MM\u0000*"}),//图片验证,tiff格式
            WMF(new int[]{215, 205, 198, 154}),//图片验证,wmf格式
            EMF(new int[]{1, 0, 0, 0, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 32, 69, 77, 70}),//图片验证,emf格式
            BMP(new int[]{66, 77}),//图片验证,nmp格式
            UNKNOWN(new byte[][]{new byte[0]});//未知魔数
        

        使用魔数校验Excel文件代码:

         private static boolean checkIsExcel(InputStream inputStream) throws IOException { //获取文件流的文件头
             FileMagic fileMagic = FileMagic.valueOf(inputStream);
             //判断Excel文件头是否符合xls或者xlsx
             if (Objects.equal(fileMagic, FileMagic.OLE2) || Objects.equal(fileMagic, FileMagic.OOXML)) { return true;
             }
             return false;
         }
        

        3.校验文件大小

        为了避免上传过大文件,影响服务器性能以及带宽。需要对文件大小进行校验,具体文件大小控制以业务为主。

        4.示例

        以校验Excel文件为例:

         /**
             * 校验文件
             *
             * @param file 文件
             * @param fileMaxSize  文件大小限制
             */
            public static void checkExcel(MultipartFile file, Double fileMaxSize) { // 文件类型判断 - 校验文件后缀
                String fileName = file.getOriginalFilename();
                if (Strings.isEmpty(fileName)) { throw new RuntimeException("文件名未找到");
                }
                String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
                if (!Objects.equal(suffix, "xls") && !Objects.equal(suffix, "xlsx")) { throw new RuntimeException("文件类型不正确,需要为xls或者xlsx");
                }
                // 文件类型判断 - 校验文件头内容
                try (InputStream inputStream = file.getInputStream()) { // 获取到上传文件的文件头信息
                    boolean isExcel = checkIsExcel(inputStream);
                    if (!isExcel) { throw new RuntimeException("文件类型不正确,原文件类型需要为xls");
                    }
                } catch (IOException e) { log.error("Get file input stream failed.", e);
                    throw new RuntimeException("文件上传失败");
                }
                // 文件大小校验 - 单位:MB
                long fileBytes = file.getSize();
                double fileSize = (double) fileBytes / 1048576;
                if (fileSize <= 0) { throw new RuntimeException("文件内容为空");
                }
                if (fileSize > fileMaxSize) { throw new RuntimeException("文件上传内容大小超出限制");
                }
            }
            /**
             * 校验文件头
             *
             * @param inputStream
             * @return
             *
             * @throws IOException
             */
            private static boolean checkIsExcel(InputStream inputStream) throws IOException { FileMagic fileMagic = FileMagic.valueOf(inputStream);
                if (Objects.equal(fileMagic, FileMagic.OLE2) || Objects.equal(fileMagic, FileMagic.OOXML)) { return true;
                }
                return false;
            }