HTML 中 files[0].type 不可靠,因其仅基于文件扩展名猜测 MIME 类型;必须通过 FileReader 读取文件头(magic bytes)前端预筛,并由服务端二次校验原始内容。
HTML 本身不提供文件类型检测能力,input[type="file"] 暴露的 files[0].type 只是浏览器根据文件扩展名“猜”的 MIME 类型,不可信,也不能替代真实内容检测。
files[0].type
这个字段由浏览器读取文件后缀(如 .jpg)查表得来,不是读文件头。用户改个后缀就能绕过:shell.php 改成 shell.jpg,files[0].type 就变成 image/jpeg,但实际是 PHP 代码。服务端若只校验这个字段,就会被上传漏洞利用。
.type 是纯客户端推测值photo.jpg.php)时该字段常为空字符串或 text/plain
application/vnd.ms-excel,而 Mac 可能报 text/csv 或 text/plain
FileReader.readAsArrayBuffer() 读 magic bytes这是唯一能在不发请求前提下可靠识别格式的方式——直接读文件前若干字节,比对已知 magic number(文件头签名)。
0xFF 0xD8)、PNG(0x89 0x50 0x4E 0x47)、GIF(0x47 0x49 0x46)、WebP(RIFF...WEBP)都够判断readAsArrayBuffer(),不能用 readAsText()(会触发编码转换,破坏二进制)file.slice(0, 12) 避免大文件阻塞主线程,也减少内存占用function detectMimeType(file) { return new Promise(resolve => { const reader = new FileReader(); reader.onload = () => { const buf = new Uint8Array(reader.result); if (buf[0] === 0xff && buf[1] === 0xd8) resolve('image/jpeg'); else if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) resolve('image/png'); else if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) resolve('image/gif'); else if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) resolve('image/webp'); else resolve('application/octet-stream'); }; reader.readAsArrayBuffer(file.slice(0, 12)); });}
Node.js 可用 file-type 库,PHP 用 finfo_file(),Python 用 mimetypes.guess_type() + python-magic。关键点是:必须传入原始文件内容(或临时文件路径),不能只看 $_FILES['f']['type'] 或后缀。
立即学习“前端免费学习笔记(深入)”;
file -i 命令底层就是靠 /usr/share/misc/magic 文件匹配 magic bytes,和前端原理一致$_FILES['f']['type'] 完全来自 HTTP 请求头的 Content-Type,可被 Burp 等工具任意篡改<input type="file" accept="image/*"> 只影响文件选择器弹窗里默认显示哪些类型,不影响实际可选文件列表(用户可点「所有文件」强行选)。它不会阻止用户选一个 .exe 文件,也不会修改 files[0].type 的值。
accept=".pdf" 会过滤掉非 PDF 图标,但 Windows 下基本无效accept 值,仅支持 image/*、video/*、audio/*
accept 当作校验依据;它只用于改善体验,降低误操作概率真正可靠的文件类型检测永远需要两层:前端快速预筛(magic bytes)+ 服务端权威判定(内容探测)。任何单侧、仅依赖后缀或 HTTP 头的方案,在生产环境里都等于没设防。