【解决方案 十二】一文彻底解决文件格式判别问题

简介: 【解决方案 十二】一文彻底解决文件格式判别问题

最近做的工作有一部分需求,需要对读取到的文件做验证,只允许上传规定格式的文件(word),不仅仅是通过后缀,还需要验证文件的真实格式,因为某些有害脚本通过改写后缀同样能上传成功,这个时候需要做的就是通过字符流来验证文件的真实格式。从网上找了很多资料和方法,有些帮助,但也并不全面,于是站在前人的基础上,通过文件编码分析,来完成这样一篇博客。本文推荐的阅读逻辑结构如下:

  • 读取文件并通过文件后缀首先排除部分非格式要求内容
  • 读取文件头部信息并通过头部十六进制进行格式鉴别
  • 仅通过文件头部信息鉴别不了的通过前8个字节位里的标示位来区分
  • 部分无法区分格式的文件,这部分希望大家探讨补充

好了,那么接下来进行正文,文件格式分析,本文采用编程语言是C#,但也同样适用于Java。同时提前介绍一个文件分析利器,Ultra,它看,可以快速将文件以二进制或十六进制编码的方式展示。

1 读取文件并通过后缀判断

这部分代码存在的意义就是,可以过滤大部分非恶意的文件后缀,因为读取文件头并转换为十六进制位比较耗性能,所以首先通过后缀过滤可以防止这部分后缀不符合要求的文件入库。这部分为主执行逻辑,用于获取文件类型

/// <summary>
        /// 判断当前文件的文件格式是否为word格式
        /// </summary>
        /// <returns></returns>
        public FileTypeEnum GetFileType()
        {
            //首先通过文件名后缀进行判断,抛出当前类型
            var fileFromClient = System.Web.HttpContext.Current.Request.Files[0];  //获取当前文件
            var fileName = fileFromClient.FileName.ToUpper();//获取文件名并将文件名转成大大写的格式来判断
            var fileSuffixName = fileName.Substring(fileName.LastIndexOf('.') + 1);//获取文件的后缀名
            if (fileSuffixName.Equals("DOCX") || fileSuffixName.Equals("DOC"))
            {
                //如果通过了后缀名验证,为防止修改后缀骗过验证,则进行文件字符流验证
                if (GetFileHeader(fileFromClient.InputStream) == OfficeFileClassEnum.DOCX ||
                    GetFileHeader(fileFromClient.InputStream) == OfficeFileClassEnum.DOC)
                {
                    return FileTypeEnum.Word;
                }
            }
            return FileTypeEnum.Unknown;
        }

当然文件类型用枚举值来列举比较好,目前仅仅对Word文件进行校验,如果需要其他格式可以进行扩展。

/// <summary>
    /// 上传的文件格式
    /// </summary>
    public enum FileTypeEnum
    {
        Word, // word格式的文件
        Unknown, // 未知文件格式
    }

2 通过头部十六进制进行格式鉴别

当然,对于部分恶意修改后缀的情况我们需要进行继续判定,通过获取文件头的十六进制格式其实就可以读取文件的真实类型,这些类型和规范都是各大厂商提前设定好的。这么做的依据来自如下实验:我创建了一个docx结尾的文档,然后拷贝一个副本,改了后缀,用工具UltraCompare打开,可以看到,二进制编码一模一样,所以仅仅修改后缀并不会更改文件的真实格式,所以通过文件头部信息读取确实能解决真正的区分文件格式的问题。

那么当然有人问了,我转个格式再传上去看你怎么判断,的确没法判断,但是大哥,你转了格式的文件还能用么,我的本质逻辑就是获取有用文件的正确格式,所以逻辑并不违背。言归正传,以下这部分代码用于依据十六进制文件头的信息来判断文件格式类型

/// <summary>
        /// 依据十六进制文件头的信息来判断文件格式类型
        /// </summary>
        /// <param name="fileStream"></param>
        /// <returns></returns>
        public OfficeFileClassEnum GetFileHeader(Stream fileStream)
        {
            //初始化文件头十六进制字符串和返回的文件格式类型
            string fileCode;
            var byteArray = new byte[8];   //设置二进制数组
            try
            {
                fileStream.Read(byteArray, 0, 8);  //读取文件头前8位字节数组数组
                fileCode = ByteToHexStr(byteArray).ToLower().Replace(" ", "");//将读取到的文件头二进制数组转为十六进制字符串并全部转换为小写并去掉全部空格
                if (fileCode.IsNullOrEmpty()) return OfficeFileClassEnum.Unknown;
            }
            catch (Exception)
            {
                return OfficeFileClassEnum.Unknown;
            }
            //如果顺利取出前8个字符生成的十六进制字符串
            var fileCodeHeaderType = fileCode.Substring(0, 8);//取字符串前8个十六进制位(4个字节位)进行判断
            //对文件头进行简单判断
            if (fileCodeHeaderType.Equals("504b0304"))     //前4个字节体现为文件头部信息,可能存在DOCX和ZIP
            {
                var checkZipOrDocx = fileCode.Substring(12, 2);  //获取第七个字节位的字符信息
                if (checkZipOrDocx.Equals("06"))
                {
                    return OfficeFileClassEnum.DOCX;
                }
                if (checkZipOrDocx.Equals("00"))
                {
                    return OfficeFileClassEnum.ZIP;
                }
            }
            if (fileCodeHeaderType.Equals("d0cf11e0"))        //前4个字节体现为文件头部信息,可能存在DOC和XLS
            {
                return OfficeFileClassEnum.DOC;
            }
            return OfficeFileClassEnum.Unknown;
        }

我们同样使用枚举类来标识真实的后缀文件

/// <summary>
    /// 后缀文件格式判别
    /// </summary>
    public enum OfficeFileClassEnum
    {
        DOCX,       //DOCX是微软2007之后的格式,用于Word文件格式
        DOC,        //DOC是微软office 97-03的存储规范,用于Word文件格式
        ZIP,        //ZIP格式的文件
        Unknown,    //  未知格式
    }

当然仅仅通过fileStream读取到的二进制字符较长,转为十六进制查看较为便捷。这部分代码如下:

/// <summary>
        /// 字节数组转16进制字符串
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public string ByteToHexStr(byte[] bytes)
        {
            var hexString = string.Empty;
            if (bytes == null || bytes.Length <= 0) return hexString;
            foreach (var item in bytes)
            {
                hexString += item.ToString("X2");
            }
            return hexString;
        }

那么问题来了,小伙子们一定会问,为啥读取了8个字节却先用4个字节判断呢,其实大多数情况下4个字节标识文件头部信息,8个字节对于某些格式的文件来说就已经涉及到内容了,如果完全按照8个字节去判别,会造成误判。那取8个字节的意义其实就体现在更加细微的区分上。这里奉上从网上搜集的文件头部格式标识(前4个字节):

  • JPEG (jpg),文件头:FFD8FF
  • 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 Word/Excel (xlsx.or.docx),文件头:504B0304
  • 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

看完这些文件头你就会发现,问题来了,zip格式的文件头和word的docx一样啊,这个怎么区分啊。这是为什么呢,因为DOCX和XLSX本质上就是一个压缩文件呀。修改docx后缀(并不改变该文件真实格式),然后用压缩工具查看,可以看到清晰的目录结构。

3 通过前8个字节位里的标示位

上面说到,没法通过文件头的四个十六进制位区分docx和zip,这个时候怎么办呢?没办法,只能查看ZIP文件的格式。于是从网上找到这篇文章:

ZIP文件格式分析 https://blog.csdn.net/a200710716/article/details/51644421

得出zip格式文件头应该有这样的意义标识:

然后终于发现了zip和docx的一点区别:第七个字节位,zip为00,而docx为06。

二者的含义也很清楚,比特位为00标识加密,为06表示强加密,所以这就是普通zip和word的区别。

4 无法区分的情况

如果真有别有用心的人,对zip进行强加密,前提是他得知晓我的判断逻辑,那确实没办法了,还有一个就是Excel和word也无法区分,这些在咨询了大佬之后,觉得似乎可以从内核层面去判断,但是基于当前业务,其实也已经够用了。不再做深究了吧。当然,交付使用前,千万别忘了单元测试:

[TestClass]
    public class WordFileHelperTest
    {
        [TestMethod]
        public void GetFileHeaderTest()
        {
            Stream docStream = new FileStream("D:\\doc.doc", FileMode.Open);   //验证doc文件
            Stream xlsStream = new FileStream("D:\\MsoIrmProtector.xls", FileMode.Open);//验证XLS文件
            Stream docxStream = new FileStream("D:\\docx.docx", FileMode.Open);//验证docx文件
            Stream xlsxStream = new FileStream("D:\\xlsx文件2.xlsx", FileMode.Open);//验证XLSX文件
            Stream zipStream = new FileStream("D:\\xlsx文件2.zip", FileMode.Open);//验证zip文件
            var docType = WordFileHelper.Instance.GetFileHeader(docStream);
            var xlsType = WordFileHelper.Instance.GetFileHeader(xlsStream);
            var docxType = WordFileHelper.Instance.GetFileHeader(docxStream);
            var xlsxType = WordFileHelper.Instance.GetFileHeader(xlsxStream);
            var zipType = WordFileHelper.Instance.GetFileHeader(zipStream);
            Assert.AreEqual(docType, OfficeFileClassEnum.DOC);
            Assert.AreEqual(xlsType, OfficeFileClassEnum.DOC);
            Assert.AreEqual(docxType, OfficeFileClassEnum.DOCX);
            Assert.AreEqual(xlsxType, OfficeFileClassEnum.DOCX);
            Assert.AreEqual(zipType, OfficeFileClassEnum.ZIP);
        }
    }

这段时间在新的团队里怎么说呢,虽然比较辛苦些,但能学到,研究到的东西也蛮多的,对成长还是很有帮助的吧,最起码搞明白了一些文件的格式和协议,如何进行安全设置。总结下,希望对大家有所帮助。事儿就这样成了

相关文章
|
4月前
|
存储 C语言
【C深度解剖】计算机数据下载和删除原理
【C深度解剖】计算机数据下载和删除原理
|
1月前
|
数据采集 机器人 计算机视觉
一手训练,多手应用:国防科大提出灵巧手抓取策略迁移新方案
【10月更文挑战第24天】国防科技大学研究人员提出了一种新颖的机器人抓取方法,通过学习统一的策略模型,实现不同灵巧夹具之间的策略迁移。该方法分为两个阶段:与夹具无关的策略模型预测关键点位移,与夹具相关的适配模型将位移转换为关节调整。实验结果显示,该方法在抓取成功率、稳定性和速度方面显著优于基线方法。论文地址:https://arxiv.org/abs/2404.09150
36 1
|
7月前
|
存储 算法 Go
ZIP文件实战指南:读写操作一网打尽
ZIP文件实战指南:读写操作一网打尽
336 0
|
缓存 Kubernetes 负载均衡
K8s有损发布问题探究
应用发布过程往往出现流量有损,本次文章内容通过提出问题、问题分析和解决方案,EDAS在面对上述问题时,提供了无侵入式的解决方案,无需更改程序代码或参数配置,在EDAS控制台即可实现应用无损上下线。
1073 10
K8s有损发布问题探究
|
XML Java 数据库连接
工作几年了,原来我只用了数据校验的皮毛~
前言 什么是 JSR-303? 添加依赖 内嵌的注解有哪些? 如何使用? 简单校验 分组校验 嵌套校验 如何接收校验结果? BindingResult 接收 全局异常捕捉 spring-boot-starter-validation做了什么? 如何自定义校验? 自定义校验注解 自定义校验器 演示 总结
|
消息中间件 存储 Java
【Java深层系列】「技术盲区」让我们一起去挑战一下如何读取一个较大或者超大的文件数据!
【Java深层系列】「技术盲区」让我们一起去挑战一下如何读取一个较大或者超大的文件数据!
152 0
【Java深层系列】「技术盲区」让我们一起去挑战一下如何读取一个较大或者超大的文件数据!
|
存储 编解码 算法
压缩的必要性和可能性(下)| 学习笔记
快速学习压缩的必要性和可能性(下),介绍了压缩的必要性和可能性(下)系统机制, 以及在实际应用过程中如何使用。
压缩的必要性和可能性(下)| 学习笔记
|
缓存 Kubernetes 负载均衡
K8s 有损发布问题探究
下文将通过对 EDAS 客户真实场景的归纳,从 K8s 的流量路径入手,分析有损发布产生的原因,并提供实用的解决方案。
K8s 有损发布问题探究
|
算法 Oracle Java
深度剖析 | 【JVM深层系列】[HotSpotVM研究系列] JVM调优的"标准参数"的各种陷阱和坑点分析(攻克盲点及混淆点)「 1 」
深度剖析 | 【JVM深层系列】[HotSpotVM研究系列] JVM调优的"标准参数"的各种陷阱和坑点分析(攻克盲点及混淆点)「 1 」
139 0
|
人工智能 算法
为什么很少有游戏支持场景破坏?是因为技术问题吗?
最近很多游戏狂热迷们正火热讨论的一个问题是:为什么很少有游戏支持场景破坏?说实话小编也非常好奇,于是乎小编去查了好多资料。接下来小编带领大家一起去深挖究竟!
149 0
为什么很少有游戏支持场景破坏?是因为技术问题吗?