先记住这个答案
浏览器按照文件扩展名或创建参数给出Blob.type,不解析内容。type为空字符串表示未识别,仍是合法Blob。判断实际类型应读取文件头部字节,比对已知魔数(如FF D8 FF是JPEG),并考虑无固定签名的格式。这个结果用于辅助校验,不能替代服务端策略。
- type源于名字或声明,不是内容指纹
- 空值读取头字节,用签名可比对
- 签名嗅探有误报,需白名单兜底
type属性如何形成与空值来源
浏览器创建Blob或File时,type值来自构造函数参数,或文件选择控件按扩展名查询系统映射表,不会读取内容字节。PNG改名.txt后type变text/plain,换成冷门后缀则返回空字符串。空字符串不是错误,仅表示当前环境无法凭扩展名归类这个文件。
既然不读文件体,浏览器就不会为Blob做内容嗅探。不存在一个标准方法来猜测未知类型。开发者只能在拿到Blob后自行读取头部数据:用FileReader.readAsArrayBuffer或blob.arrayBuffer()。两种异步方式都能取到前几KB,然后将字节与已知魔数比较。
常见二进制格式都有签名,如GIF以GIF87a/GIF89a开头,PNG固定89 50 4E 47。文本类文件没有唯一前缀,只能按内容试探。因此,字节比对只适合有明确魔数的格式,对未知扩展名仍不可靠。
头像上传时用字节签名校验
设头像上传接口只收JPG和PNG。在Linux终端下,浏览器常给文件空type,用户手工改名也可能伪造。前端先用file.slice(0, 16)截取片段,转成ArrayBuffer,检查是否以FF D8 FF或89 50 4E 47开头。若都不匹配,就用“文件内容不是有效图片”提示并中断提交。
这个方法能拦截“文本文件强制改成.jpg”的情况,但对合法但无签名的SVG会误拒。要避免误伤,需要产品先定白名单:只允许有魔数签名且你检测过的格式,其余一律拒绝。若必须支持SVG,需要单独判断XML标签,而不能用通用嗅探。
读取头部的操作是异步的,要防止用户重复点击提交。建议在表单校验态里把文件对象缓存,依次await校验后再发请求。因为只读几十字节,大文件也不会撑爆内存,但代码需要处理ArrayBuffer的边界与字节对齐。
内容嗅探的失效条件与缓解
内容嗅探失效的典型场景是无固定签名的文本类型:text/plain、application/json、text/html都可能以空字节开头或带BOM。若想识别它们,需要解析内容结构,这会引入极大的不确定性与计算成本,不适合做通用方案。
另一个边界是同一签名对应多种子格式:ZIP文件被Office套件及安卓APK共同使用,仅凭首字节无法区分它们。WebP内部还有VP8/VP8L分支。当签名不够特异时,嗅探结论只能精确到容器级,而不是最终子类型。
面对这些失效,工程上应由后端在接收后做权威解码,前端只把Blob.type和字节签名作为预筛选。空type时不要提示“文件损坏”,应说“无法自动识别格式,请确认文件来源”。这既诚实,也避免误导用户去重传同一文件。
容易答错的地方
- type为空表示文件有问题
- 空字符串只是没有识别出MIME,不代表数据损坏或不可用。浏览器可能因扩展名未注册、用户重命名而给空值。很多合法
Blob(如File对象)在构造时可任意指定type,空值也完全允许。 - 内容嗅探能替代type做安全校验
- 浏览器不保证嗅探正确,且没有标准实现。自写比对只能覆盖已知魔数,容易漏判或误判。攻击者可构造同时满足两种签名的文件,所以嗅探只能作为体验优化,不能作为安全边界。后端必须独立验证。
面试官还会怎么问?
具体如何读取Blob的前几个字节?
可以用blob.slice(0, 4).arrayBuffer()取到前4字节,然后转成Uint8Array和魔数常量比较。兼容旧浏览器用FileReader.readAsArrayBuffer,但arrayBuffer()在现代环境更简洁。注意需要处理异步错误。
多数图片类型都有签名吗?
常见有,JPEG、PNG、GIF、WebP、BMP都有固定前缀。但像SVG是XML文本,没有固定前几字节,不能仅靠签名判断。音频中WAV有RIFF头,MP3虽有ID3但可偏移,需要容错扫描。
Blob.type和File.type可否都当作扩展名推断?
可以,File继承自Blob,两者type生成规则一样,都基于文件名扩展名。因此改扩展名会让两者给出一样的错误结果,不能因为来自File就信任。真正的可靠性来自后续字节读取和服务器解析。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。