不能只靠文件扩展名或Content-Type验证,因其可被客户端任意伪造;必须通过读取文件前16–32字节的magic number判断真实类型,推荐使用FileTypeDetector等可靠库,并返回415或422状态码明确错误语义。

为什么不能只靠文件扩展名或Content-Type做验证
浏览器传来的 Content-Type(即 MIME 类型)完全由客户端决定,可任意伪造;扩展名更不可信。攻击者只要把恶意可执行文件改成 .jpg 就能绕过纯后缀检查。真实类型必须从文件字节头(magic number)判断。
常见误操作包括:
- 仅用
Path.GetExtension(fileName)匹配白名单 - 直接信任
IFormFile.ContentType - 读取整个文件再分析(浪费内存、阻塞 I/O)
如何用 C# 读取前几个字节判断文件类型
核心思路:只读取前 16–32 字节(足够覆盖绝大多数 magic header),比对已知签名。不要加载全文件。
关键点:
- 使用
IFormFile.OpenReadStream()获取流,避免内存拷贝 - 调用
stream.ReadAsync(buffer, 0, buffer.Length)限制长度(如 32 字节) - 用
Memory或Span做无分配比对 - 注意 PNG/JPEG/GIF 等格式的 signature 位置和长度(如 JPEG 是
FF D8 FF开头,PNG 是89 50 4E 47)
示例片段(简化逻辑):
byte[] header = new byte[32];
await file.OpenReadStream().ReadAsync(header, 0, header.Length);
if (header.AsSpan().StartsWith(new byte[] { 0xFF, 0xD8, 0xFF })) {
// JPEG
} else if (header.AsSpan().StartsWith(new byte[] { 0x89, 0x50, 0x4E, 0x47 })) {
// PNG
}有哪些现成可靠的 MIME 推断库可直接用
自己维护 magic number 表容易漏判、难覆盖边缘格式(如 WebP、AVIF、Office 文档)。推荐两个轻量方案:
-
FileTypeDetector(NuGet 包FileTypeDetector):专注 header 检测,无依赖,支持 100+ 类型,API 极简:var detector = new FileTypeDetector(); using var stream = file.OpenReadStream(); var result = await detector.DetectFileTypeAsync(stream); // 返回 MimeType 和 Confidence if (result.MimeType == "image/jpeg" && result.Confidence > 0.9) { ... } Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream配合自定义检测器:适合需要深度控制流生命周期的场景,但需自行管理 buffer 复位(stream.Position = 0后才能传给后续处理)
注意:别用 System.Drawing.Common 加载图片验证——它不校验 header,且在非 Windows 环境可能崩溃,还吃内存。
验证失败时该返回什么错误码和提示
HTTP 层应返回明确语义的状态码,而非笼统的 400:
-
415 Unsupported Media Type:MIME 不在允许列表内(如传了application/x-executable) -
422 Unprocessable Entity:文件头与扩展名冲突(如.pdf但 header 是 JPEG) - 日志中必须记录原始
ContentType、检测出的 MIME、文件名、header 前 8 字节十六进制(用于事后审计)
切忌只返回“文件类型不合法”这种模糊提示——前端无法据此友好提示用户,也掩盖了真实攻击尝试。
文件头验证不是银弹。ZIP 类压缩包、加密 PDF、带元数据的 TIFF 都可能绕过简单 signature 检查。真要防住高级攻击,得配合沙箱解析或服务端反病毒扫描。










