最可靠方案是读取文件头(Magic Number)而非依赖扩展名;C#中可用File.ReadAllBytes读前4–8字节比对已知签名,避免使用Image.FromFile等高风险方法。

用 File.ReadAllBytes 读前几个字节判断图片类型最稳
靠扩展名(.jpg、.png)判断文件是否为图片完全不可信——用户改个后缀就能绕过。真正可靠的方案是读取文件头(Magic Number)。C# 没有内置的“识别图片类型”函数,但只需要读取前 4–8 字节,比对已知签名即可。
注意:不要用 Image.FromFile 或 Bitmap.Decode 去尝试加载,一来性能差(尤其大文件),二来可能抛出异常(如损坏文件)、触发 GDI+ 内存泄漏,甚至被恶意构造的文件头导致进程崩溃。
-
File.ReadAllBytes简单直接,适合小文件(FileStream.Read 读前 16 字节即可 - 常见图片格式只需比对前 2–4 字节:JPEG 是
0xFF 0xD8,PNG 是0x89 0x50 0x4E 0x47,GIF 是0x47 0x49 0x46(即 "GIF") - WebP 较特殊:有 VP8/VP8L/VP8X 三种子格式,通用签名为
0x52 0x49 0x46 0x46 ?? ?? ?? ?? 0x57 0x45 0x42 0x50(即 "RIFF****WEBP"),需检查第 9–12 字节是否为0x57 0x45 0x42 0x50
System.IO.Path.GetExtension 不能替代文件头校验
很多人图省事,写 Path.GetExtension(path).ToLower() == ".jpg" 就完事。这在上传表单、日志分析等低风险场景看似可行,但一旦涉及用户可控输入(如 Web 文件上传、本地导入),就等于把类型判断权交给了客户端。
典型翻车场景:
- 用户把木马.exe 改名为
photo.jpg→ 后端误判为图片,后续执行解析逻辑时崩溃或提权 - 某些编辑器保存 PNG 时未写标准头(比如缺
0x89),扩展名正确但实际不是合法 PNG - Base64 转存的图片可能被保存为
.txt,扩展名失效但内容仍是图片
所以,GetExtension 只能作辅助提示或快速过滤,绝不能作为唯一判断依据。
别直接用 Image.FromStream 做类型检测
有人会想到打开流再调用 Image.FromStream(stream),看是否抛异常。这方法表面可行,实则隐患极多:
- 该方法会尝试解码整个图像元数据,遇到畸形 EXIF 或超长 ICC Profile 时极易 OOM 或卡死
- 某些格式(如 TIFF)支持多页、多种压缩方式,
FromStream可能只读前几 KB 就报错,误判为非图片 - 它不区分“不是图片”和“图片损坏”,错误信息统一为
OutOfMemoryException或ArgumentException,无法精准归因 - 每次调用都会创建 GDI+ 对象,若并发高且没及时
Dispose,容易触发System.OutOfMemoryException: Out of memory.
真要走流式解析,也应先用文件头筛一遍,再交给 Image 类处理可信来源的文件。
推荐封装一个轻量 IsImageHeaderValid 工具方法
把 Magic Number 判断逻辑收拢成一个静态方法,比零散 if-else 更易维护、测试和复用。下面是一个生产可用的精简版(覆盖 JPG/PNG/GIF/BMP/WebP):
public static bool IsImageHeaderValid(string path)
{
if (!File.Exists(path) || new FileInfo(path).Length var header = File.ReadAllBytes(path).Take(12).ToArray();
if (header.Length = 12 &&
header[0] == 0x52 && header[1] == 0x49 && header[2] == 0x46 && header[3] == 0x46 &&
header[8] == 0x57 && header[9] == 0x45 && header[10] == 0x42 && header[11] == 0x50)
return true;
return false;
}
注意 WebP 的判断必须确保数组长度 ≥12,否则 header[8] 会越界;BMP 只需前两字节 BM,但实际文件头更长,这里取最小必要条件即可。
复杂点在于 WebP 和 AVIF 等新格式的变体越来越多,如果业务需要支持更多类型,建议用 ImageSharp 的 Image.DetectFormat(纯托管、无 GDI+ 风险),但要注意它仍会读取部分流——务必传入可重置的 MemoryStream 或带 CanSeek=true 的流。










