文件校验需分层:先校扩展名和magic number,再用memorystream缓存并重置位置供后续处理;大文件须限大小、用流式解析;语义校验放业务层;敏感扫描逐块匹配;混合校验应拆解为独立可控步骤。

上传文件前必须校验 ContentType 和扩展名是否匹配
用户传来的 ContentType 完全不可信,浏览器能随意伪造。只靠 FileName 后缀或 ContentType 判断类型,等于把校验大门敞开。
实操建议:
- 先用
Path.GetExtension(fileName)提取后缀,转小写后比对白名单(如".pdf"、".xlsx") - 再读取文件前几个字节(Magic Number),用
FileStream.ReadAsync读取最多 8 字节,比对真实格式签名(如 PDF 是%PDF,PNG 是\x89PNG) - 拒绝
ContentType为"text/plain"却声称是.docx的请求——这大概率是绕过前端限制的尝试
IFormFile 流不能重复读取,校验逻辑要一次性完成
IFormFile.OpenReadStream() 返回的流默认不支持 Seek,一旦读完就无法 rewind。如果先读一遍校验内容,再交给业务逻辑处理,会抛出 NotSupportedException: Stream does not support seeking。
实操建议:
- 用
MemoryStream缓存原始字节:调用file.CopyToAsync(memoryStream)一次读完 - 所有断言(大小、类型、敏感词扫描、结构解析)都在该
memoryStream上做,用memoryStream.Position = 0重置位置 - 避免在中间件或过滤器里只做部分校验,留一半给 Controller——流可能已被消费
业务规则断言要分层:基础层用 FileStream,语义层用解析后对象
比如上传 Excel 表格,仅检查是不是 .xlsx 文件远远不够;用户可能上传空表、列名错位、金额字段含非法字符——这些必须进业务逻辑才看得清。
实操建议:
- 基础断言(大小、扩展名、Magic Number)放在 Action Filter 或自定义
ModelBinder中快速拦截 - 语义断言(如“第3列必须是大于0的整数”、“客户编号不能重复”)必须在 Controller 或 Service 层,用
ExcelDataReader或EPPlus解析后校验 - 别在
Assert里 throwArgumentException,改用ModelState.AddModelError,让框架统一返回 400 和错误详情
大文件上传时,Assert 要防内存爆炸和超时
直接 CopyToAsync(new MemoryStream()) 处理 500MB 文件,会瞬间吃光服务器内存;而逐行解析 CSV 或 XML 时,没设限的正则或递归解析可能卡死线程。
实操建议:
- 用
file.Length做第一道闸门,超过阈值(如 10MB)直接return BadRequest("文件过大") - 解析类文件(CSV/JSON/XML)时,用流式 API:
CsvReader(CsvHelper)、JsonDocument.ParseUtf8(不加载全文)、XmlReader(非XDocument) - 敏感内容扫描(如身份证、手机号)别用全局正则
Regex.Matches,改用Regex.Match逐块扫描,匹配到即停
真正麻烦的是混合校验:既要 Magic Number,又要解密 ZIP 内某个 XML 的特定节点,还要查数据库去重。这种链路没法塞进一个 Assert 方法里,得拆成可测、可超时控制、可打日志的独立步骤。










