为什么不能只靠文件扩展名或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<byte></byte>或
Span<byte></byte>做无分配比对 注意 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:文件头与扩展名冲突(如
ContentType、检测出的 MIME、文件名、header 前 8 字节十六进制(用于事后审计)
切忌只返回“文件类型不合法”这种模糊提示——前端无法据此友好提示用户,也掩盖了真实攻击尝试。
文件头验证不是银弹。ZIP 类压缩包、加密 PDF、带元数据的 TIFF 都可能绕过简单 signature 检查。真要防住高级攻击,得配合沙箱解析或服务端反病毒扫描。
