最近做的工作有一部分需求,需要对读取到的文件做验证,只允许上传规定格式的文件(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);}}

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

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

  1. JavaScript——易班优课YOOC课群在线测试自动答题解决方案(十二)脚本整合

    前文 JavaScript--易班优课YOOC课群在线测试自动答题解决方案(一)答案获取 Spring Boot--易班优课YOOC课群在线测试自动答题解决方案(二)答案储存 Spring Boot- ...

  2. Java多线程学习四十二:有哪些解决死锁问题的策略和哲学家就餐问题

    线上发生死锁应该怎么办 如果线上环境发生了死锁,那么其实不良后果就已经造成了,修复死锁的最好时机在于"防患于未然",而不是事后补救.就好比发生火灾时,一旦着了大火,想要不造成损失去 ...

  3. 爬虫入门经典(十二) | 一文带你快速爬取豆瓣电影

      大家好,我是不温卜火,是一名计算机学院大数据专业大三的学生,昵称来源于成语-不温不火,本意是希望自己性情温和.作为一名互联网行业的小白,博主写博客一方面是为了记录自己的学习过程,另一方面是总结自己 ...

  4. SpringCloud学习笔记(十二)基于Hystrix解决雪崩效应

    专辑目录:SpringCloud学习日志 什么是雪崩效应? 当请求数量远超出服务器承受能力或者服务器无法再处理请求时,导致请求等待时间比较久,也就是平常说的卡.由于微服务是利用RPC相互请求的,所以可 ...

  5. Burpsuite【十二模块一次解决】【这都不看?】Filter、Target、Scanner、Proxy、Intruder、Repeater、Sequencer、Decoder、Comparer…

    目录 Filter showing all items(过滤器) 筛选功能: 过滤器在哪呢? 你使用过滤器了嘛? Target(目标) URL不同颜色,它的区别是? 如何查看一个/多个指定的URL地址 ...

  6. JavaScript——易班优课YOOC课群在线测试自动答题解决方案(十五)整合升级+引入jQuery

    前文 JavaScript--易班优课YOOC课群在线测试自动答题解决方案(一)答案获取 Spring Boot--易班优课YOOC课群在线测试自动答题解决方案(二)答案储存 Spring Boot- ...

  7. JavaScript——易班优课YOOC课群在线测试自动答题解决方案(十九)强制重做

    前文 JavaScript--易班优课YOOC课群在线测试自动答题解决方案(一)答案获取 Spring Boot--易班优课YOOC课群在线测试自动答题解决方案(二)答案储存 Spring Boot- ...

  8. JavaScript + Thymeleaf + Spring Boot——易班优课YOOC课群在线测试自动答题解决方案(十八)模板脚本

    前文 JavaScript--易班优课YOOC课群在线测试自动答题解决方案(一)答案获取 Spring Boot--易班优课YOOC课群在线测试自动答题解决方案(二)答案储存 Spring Boot- ...

  9. JavaScript——易班优课YOOC课群在线测试自动答题解决方案(十六)利用PC端和移动端BUG

    前文 JavaScript--易班优课YOOC课群在线测试自动答题解决方案(一)答案获取 Spring Boot--易班优课YOOC课群在线测试自动答题解决方案(二)答案储存 Spring Boot- ...

最新文章

  1. 为什么对高斯分布的方差的极大似然估计是有偏的?
  2. 【ACM】杭电OJ 1076
  3. table的分页打印
  4. 数据结构(C语言实现》课后1-5章习题答案
  5. OpenSSL--Window生成证书实战
  6. C++学习之路 | PTA乙级—— 1093 字符串A+B (20 分)(精简)
  7. flutter SlideTransition实现平移动画
  8. 【Python】python帮助文档
  9. 魔兽争霸修改器,局域网内使用!防封号!!!!!!!!
  10. 倍福TwinCAT软件安装及注意事项
  11. catia二次开发c语言,CATIA二次开发1_VB语言基础语法
  12. 关于python通过pyautocad操作cad的2021-06-10
  13. php implode key,PHP implode()用法及代碼示例
  14. 百度地图之标注物聚合
  15. 快应用JS自定义月相变化效果
  16. 计算机程序设计论文2万字,【程序设计论文】核心素养下的计算机程序设计教学(共4049字)...
  17. 命令行登陆北邮校园网
  18. 37 篇! Facebook 今年被 CVPR 收录的论文都说了啥?
  19. Python 使用 matplotlib绘制3D图形
  20. 枯树洒落的泪花,心却不知飞向何

热门文章

  1. 关于Value ‘0000-00-00 00:00:00‘ can not be represented as java.sql.Timestamp异常问题的解读
  2. Arduino简单实现两自由度Scara机器人
  3. 解读如何打造支撑万亿规模的K8s集群?
  4. 成都信息工程大学计算机分数线,成都信息工程大学录取分数线2021是多少分(附历年录取分数线)...
  5. mybatis-plus insertStrategy、updateStrategy、whereStrategy属性
  6. Win7系统组策略怎么打开 打开组策略的几种方法
  7. android根据滑动字体颜色被填充,自定义View:02-滑动变色的字体
  8. Go语言slice详解
  9. html表格填充空白单元格,快速填充空白单元格【应对步骤】
  10. xj膜你赛(n-1)