【解決方案 十二】一文徹底解決文件格式判別問題

最近做的工作有一部分需求,需要對讀取到的文件做驗證,只允許上傳規定格式的文件(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);
        }
    }

這段時間在新的團隊裏怎麼說呢,雖然比較辛苦些,但能學到,研究到的東西也蠻多的,對成長還是很有幫助的吧,最起碼搞明白了一些文件的格式和協議,如何進行安全設置。總結下,希望對大家有所幫助。事兒就這樣成了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章