Selaa lähdekoodia

!1031 增强文件上传安全,防止客户端伪造文件
👍Merge pull request !1031 from FunCoder/next

zuohuaijun 2 vuotta sitten
vanhempi
commit
020172cc4c

+ 10 - 3
Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs

@@ -241,8 +241,9 @@ public class SysFileService : IDynamicApiController, ITransient
     /// </summary>
     /// <param name="file">文件</param>
     /// <param name="savePath">路径</param>
+    /// <param name="allowSuffix">允许的格式,比如 .jpg.png.gif.tif.bmp </param>
     /// <returns></returns>
-    private async Task<SysFile> HandleUploadFile(IFormFile file, string savePath)
+    private async Task<SysFile> HandleUploadFile(IFormFile file, string savePath, string allowSuffix = "")
     {
         if (file == null) throw Oops.Oh(ErrorCodeEnum.D8000);
 
@@ -295,6 +296,12 @@ public class SysFileService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(suffix))
             throw Oops.Oh(ErrorCodeEnum.D8003);
 
+        //增强安全,防止客户端伪造文件
+        if (!string.IsNullOrWhiteSpace(allowSuffix) && !allowSuffix.Contains(suffix))
+            throw Oops.Oh(ErrorCodeEnum.D8003);
+        if (!VerifyFileExtensionName.IsSameType(file.OpenReadStream(), suffix))
+            throw Oops.Oh(ErrorCodeEnum.D8001);
+
         var newFile = new SysFile
         {
             Id = YitIdHelper.NextId(),
@@ -384,7 +391,7 @@ public class SysFileService : IDynamicApiController, ITransient
     [DisplayName("上传头像")]
     public async Task<SysFile> UploadAvatar([Required] IFormFile file)
     {
-        var sysFile = await UploadFile(file, "Upload/Avatar");
+        var sysFile = await HandleUploadFile(file, "Upload/Avatar", ".jpg.png.gif.tif.bmp");
 
         var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
         var user = sysUserRep.GetFirst(u => u.Id == _userManager.UserId);
@@ -406,7 +413,7 @@ public class SysFileService : IDynamicApiController, ITransient
     [DisplayName("上传电子签名")]
     public async Task<SysFile> UploadSignature([Required] IFormFile file)
     {
-        var sysFile = await UploadFile(file, "Upload/Signature");
+        var sysFile = await HandleUploadFile(file, "Upload/Signature", ".jpg.png.gif.tif.bmp");
 
         var sysUserRep = _sysFileRep.ChangeRepository<SqlSugarRepository<SysUser>>();
         var user = sysUserRep.GetFirst(u => u.Id == _userManager.UserId);

+ 156 - 0
Admin.NET/Admin.NET.Core/Util/VerifyFileExtensionName.cs

@@ -0,0 +1,156 @@
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.Serialization;
+
+namespace Admin.NET.Core;
+
+public static class VerifyFileExtensionName
+{
+    private static IDictionary<string, string> dics_ext = new Dictionary<string, string>();
+    private static IDictionary<string, HashSet<int>> ext_dics = new Dictionary<string, HashSet<int>>();
+
+    static VerifyFileExtensionName()
+    {
+        dics_ext.Add("FFD8FFE0", ".jpg");
+        dics_ext.Add("89504E47", ".png");
+        dics_ext.Add("47494638", ".gif");
+        dics_ext.Add("49492A00", ".tif");
+        dics_ext.Add("424D", ".bmp");
+
+        //PS和CAD
+        dics_ext.Add("38425053", ".psd");
+        dics_ext.Add("41433130", ".dwg"); // CAD
+        dics_ext.Add("252150532D41646F6265", ".ps");
+
+        //办公文档类
+        dics_ext.Add("D0CF11E0", ".doc"); //ppt、doc、xls
+        dics_ext.Add("504B0304", ".docx");//pptx、docx、xlsx    
+
+        /**注意由于文本文档录入内容过多,则读取文件头时较为多变-START**/
+        dics_ext.Add("0D0A0D0A", ".txt");//txt
+        dics_ext.Add("0D0A2D2D", ".txt");//txt
+        dics_ext.Add("0D0AB4B4", ".txt");//txt        
+        dics_ext.Add("B4B4BDA8", ".txt");//文件头部为汉字
+        dics_ext.Add("73646673", ".txt");//txt,文件头部为英文字母
+        dics_ext.Add("32323232", ".txt");//txt,文件头部内容为数字
+        dics_ext.Add("0D0A09B4", ".txt");//txt,文件头部内容为数字
+        dics_ext.Add("3132330D", ".txt");//txt,文件头部内容为数字      
+        /**注意由于文本文档录入内容过多,则读取文件头时较为多变-END**/
+
+        dics_ext.Add("7B5C727466", ".rtf"); // 日记本
+
+        dics_ext.Add("255044462D312E", ".pdf");
+
+        //视频或音频类
+        dics_ext.Add("3026B275", ".wma");
+        dics_ext.Add("57415645", ".wav");
+        dics_ext.Add("41564920", ".avi");
+        dics_ext.Add("4D546864", ".mid");
+        dics_ext.Add("2E524D46", ".rm");
+        dics_ext.Add("000001BA", ".mpg");
+        dics_ext.Add("000001B3", ".mpg");
+        dics_ext.Add("6D6F6F76", ".mov");
+        dics_ext.Add("3026B2758E66CF11", ".asf");
+
+        //压缩包
+        dics_ext.Add("52617221", ".rar");
+        dics_ext.Add("504B03040A000000", ".zip");
+        dics_ext.Add("1F8B08", ".gz");
+
+        //程序文件
+        dics_ext.Add("3C3F786D6C", ".xml");
+        dics_ext.Add("68746D6C3E", ".html");
+        //dics_ext.Add("7061636B", ".java");
+        //dics_ext.Add("3C254020", ".jsp");
+        //dics_ext.Add("4D5A9000", ".exe");
+
+        dics_ext.Add("44656C69766572792D646174653A", ".eml"); // 邮件
+        dics_ext.Add("5374616E64617264204A", ".mdb");//Access数据库文件
+
+        dics_ext.Add("46726F6D", ".mht");
+        dics_ext.Add("4D494D45", ".mhtml");
+        //
+        foreach (var dics in dics_ext)
+        {
+            if (!ext_dics.ContainsKey(dics.Value))
+                ext_dics.Add(dics.Value, new HashSet<int> { dics.Key.Length / 2 });
+            else
+                ext_dics[dics.Value].Add(dics.Key.Length / 2);
+        }
+    }
+    /// <summary>
+    /// 文件格式和文件内容格式是否一致
+    /// </summary>
+    /// <param name="stream"></param>
+    /// <param name="suffix"></param>
+    /// <returns></returns>
+    public static bool IsSameType(Stream stream, string suffix = ".jpg")
+    {
+        if (stream == null)
+            return false;
+        //
+        suffix = suffix.ToLower();
+        if (!ext_dics.ContainsKey(suffix))
+            return false;
+        try
+        {
+            foreach (var Len in ext_dics[suffix])
+            {
+                byte[] b = new byte[Len];
+                stream.Read(b, 0, b.Length);
+                //string fileType = System.Text.Encoding.UTF8.GetString(b);
+                string fileKey = GetFileHeader(b);
+                if (dics_ext.ContainsKey(fileKey))
+                    return true;
+            }
+        }
+        catch (IOException e)
+        {
+        }
+        return false;
+    }
+    /**
+     * 根据文件转换成的字节数组获取文件头信息
+     * 
+     * @param filePath
+     *            文件路径
+     * @return 文件头信息
+     */
+    private static string GetFileHeader(byte[] b)
+    {
+        string value = BytesToHexString(b);
+        return value;
+    }
+
+    /**
+     * 将要读取文件头信息的文件的byte数组转换成string类型表示
+     * 下面这段代码就是用来对文件类型作验证的方法, 
+     * 将字节数组的前四位转换成16进制字符串,并且转换的时候,要先和0xFF做一次与运算。
+     * 这是因为,整个文件流的字节数组中,有很多是负数,进行了与运算后,可以将前面的符号位都去掉,
+     * 这样转换成的16进制字符串最多保留两位,如果是正数又小于10,那么转换后只有一位,
+     * 需要在前面补0,这样做的目的是方便比较,取完前四位这个循环就可以终止了
+     * @param src要读取文件头信息的文件的byte数组
+     * @return 文件头信息
+     */
+    private static string BytesToHexString(byte[] src)
+    {
+        StringBuilder builder = new StringBuilder();
+        if (src == null || src.Length <= 0)
+        {
+            return null;
+        }
+        string hv;
+        for (int i = 0; i < src.Length; i++)
+        {
+            // 以十六进制(基数 16)无符号整数形式返回一个整数参数的字符串表示形式,并转换为大写
+            hv = Convert.ToString(src[i] & 0xFF, 16).ToUpper();
+            if (hv.Length < 2)
+            {
+                builder.Append(0);
+            }
+            builder.Append(hv);
+        }
+        return builder.ToString();
+    }
+}