C# 驗證檔案類型 & 讀取檔案標頭 & Stream 多次讀取 | C# validate file type & read file header bytes & Stream read multiple times

最近遇到一個防呆驗證問題,
一個檔案上傳的功能,即使有了副檔名偵測,也會有可能遇到有心人故意弄非正規檔案傳入,
那該怎麼在後端去防呆呢?

找到了以下這篇Wiki,統整了大部分檔案的File Signature (檔案特徵)
List of file signatures
其中Hex signature欄位是主要需要的資訊,可以利用讀取檔案的前幾個Byte,然後比對公版的檔案特徵值,判斷是否符合來防呆這個檔案不是允許的
這裡我只需要用到JPG、GIF、PNG的標頭就好

要注意這份清單是不固定的,可能會誕生新的格式,或者有漏,但是就是一個參考,也可以去個別檔案的規格文件查看file signature章節來設定

有些檔案會有多種標頭格式,我找到的驗證做法是取前面幾個相同的部分驗證,後面不同的部分就不驗證
例如jpg就有四種:
FF D8 FF DB
FF D8 FF E0 00 10 4A 46 49 46 00 01
FF D8 FF EE
FF D8 FF E1 ?? ?? 45 78 69 66 00 00
所以乾脆只比對前面的 FF D8

實作的話則是參考這篇stackoverflow解答:
Validate image from file in C#
主要概念是把檔案的Byte傳入方法後,根據設定好的規格長度取出byte,用SequenceEqual比對是否相符

我先建立一個通用的圖檔類型enum:

1
2
3
4
5
6
public enum ValidImageTypeEnum
{
Jpeg,
Gif,
Png
}

然後作一個根據這個enum回傳指定的byte array的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static IEnumerable<byte[]> GetImageValidateHeaders(ValidImageTypeEnum[] fileTypes)
{
foreach (var ft in fileTypes)
{
switch (ft)
{
case ValidImageTypeEnum.Jpeg:
yield return new byte[] { 0xFF, 0xD8 };
break;
case ValidImageTypeEnum.Gif:
yield return Encoding.ASCII.GetBytes("GIF");
break;
case ValidImageTypeEnum.Png:
yield return new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
break;
default:
break;
}
}
}

這裡用16進位是方便比對wiki清單,原本解答中的是放10進位的數字

再做一個把圖片byte array傳入,比對是否為圖片格式的header擴充方法

1
2
3
4
public static bool IsImage(this byte[] fileBytes, IEnumerable<byte[]> headers)
{
return headers.Any(x => x.SequenceEqual(fileBytes.Take(x.Length)));
}

然後使用上就是當你在操作Stream時把前幾個byte讀取出來後送來驗證

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IEnumerable<byte[]> validImageFileHeaders = 
ValidationHelper.GetImageValidateHeaders(new ValidateImageFileTypeEnum[]
{
ValidateImageFileTypeEnum.Gif,
ValidateImageFileTypeEnum.Jpeg,
ValidateImageFileTypeEnum.Png
});

using (var stream = file.Stream)
{
//buffer 我是設定一個很小的值,不用讀取檔案全部的內容
byte[] fileHeaderBuffer = new byte[16];
int num = stream.Read(fileHeaderBuffer, 0, 16);
if (num == 0 || !fileHeaderBuffer.IsImage(validImageFileHeaders))
{
throw new ArgumentException("圖片不是正常格式");
}

//重設stream的讀取位置以上傳,很重要不然後面上傳處理會變空檔案
stream.Position = 0;

//Do your work
}

再來就是一個很重要的

1
stream.Position = 0;

我在判斷完檔案標頭之後一直遇到圖檔上傳都變空的,查了一下才知道,
因為Stream在讀取時會改變他的讀取位置,
所以後面要繼續使用同一個Stream時必須重設位置,這樣後面在讀取時才不會從中間或尾巴讀取導致變空的檔案
這個問題卡了一小時,關鍵字不太會下…