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

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

最近做的工作有一部分需求,需要对读取到的文件做验证,只允许上传规定格式的文件(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月前
|
机器学习/深度学习 SQL 数据挖掘
ADB优化器背后的秘密:如何用成本估算和规则引擎编织高效的查询网络?
【8月更文挑战第27天】AnalyticDB (ADB) 是一款专为大规模数据集设计的高性能分析型数据库。本文深入探讨ADB的优化器如何通过成本估算、规则引擎及机器学习等策略生成高效执行计划。成本估算是选择最优路径的关键;规则引擎通过谓词下推等手段优化查询;机器学习则使优化器能基于历史数据预测执行效率。结合示例代码与执行计划分析,展现了ADB在提升查询性能方面的强大功能。未来,ADB将继续进化以满足日益增长的大数据分析需求。
41 0
|
2月前
|
运维
【运维基础知识】用dos批处理批量替换文件中的某个字符串(本地单元测试通过,部分功能有待优化,欢迎指正)
该脚本用于将C盘test目录下所有以t开头的txt文件中的字符串“123”批量替换为“abc”。通过创建批处理文件并运行,可实现自动化文本替换,适合初学者学习批处理脚本的基础操作与逻辑控制。
155 56
|
数据库 芯片
如何使用GEOquery和limma完成芯片数据的差异表达分析
如何分析芯片数据 我最早接触的高通量数据就是RNA-seq,后来接触的也基本是高通量测序结果而不是芯片数据,因此我从来没有分析过一次芯片数据,而最近有一个学员在看生信技能树在腾讯课堂发布的课程GEO数据库表达芯片处理之R语言流程遇到了问题问我请教,为了解决这个问题,我花了一个晚上时间学习这方面的分析。
4291 0
|
1月前
|
存储 JSON 算法
TDengine 检测数据最佳压缩算法工具,助你一键找出最优压缩方案
在使用 TDengine 存储时序数据时,压缩数据以节省磁盘空间是至关重要的。TDengine 支持用户根据自身数据特性灵活指定压缩算法,从而实现更高效的存储。然而,如何选择最合适的压缩算法,才能最大限度地降低存储开销?为了解决这一问题,我们特别推出了一个实用工具,帮助用户快速判断并选择最适合其数据特征的压缩算法。
41 0
|
7月前
|
人工智能 自然语言处理 测试技术
论文介绍:LLMLingua-2——面向高效忠实任务无关性提示压缩的数据蒸馏方法
【5月更文挑战第2天】LLMLingua-2是一种针对大型语言模型(LLMs)的数据蒸馏方法,旨在实现高效且忠实的提示压缩。通过从LLMs中提取知识,该方法在压缩提示的同时保持关键信息,提高模型泛化能力和效率。采用Transformer编码器,LLMLingua-2将提示压缩转化为标记分类问题,确保压缩后的提示忠实度并减少延迟。实验表明,该方法在多个数据集上优于基线,并在压缩延迟上取得显著改进,但也存在泛化能力和扩展性限制。论文链接:https://arxiv.org/abs/2403.12968
135 5
|
监控 算法
转:如何使用模糊算法提高监控软件的性能
如何才能提高监控软件的性能呢?其实,咱们可以通过模糊算法从各个角度着手,让监控系统变得更聪明更高效。模糊逻辑就是那种对付那些有点儿模糊不太确定信息的法宝,它在解决一些莫名其妙的情况时可是大显身手。在监控软件的世界里,模糊逻辑也是个大明星,可以帮助我们做出更明智的决策和更敏捷的响应,然后整个系统就会变得特别厉害!
75 0
jira学习案例124-代码分割优化性能
jira学习案例124-代码分割优化性能
82 0
jira学习案例124-代码分割优化性能
|
存储 编解码 算法
压缩的必要性和可能性(下)| 学习笔记
快速学习压缩的必要性和可能性(下),介绍了压缩的必要性和可能性(下)系统机制, 以及在实际应用过程中如何使用。
压缩的必要性和可能性(下)| 学习笔记
|
数据采集 消息中间件 分布式计算
最终整体回顾总结(代码-预处理及爬虫识别)|学习笔记
快速学习最终整体回顾总结(代码-预处理及爬虫识别)
127 0
最终整体回顾总结(代码-预处理及爬虫识别)|学习笔记
技术汇总:第十五章:MyBatisGenerator数据层代码生成
技术汇总:第十五章:MyBatisGenerator数据层代码生成
102 0