用FileReader读文件头魔数可准确识别真实类型,如JPEG(FFD8FF)、PNG(89504E47)、PDF(25504446)等,比对前4–12字节并清空非法文件;HTML需用readAsText配合正则检测高危标签与脚本。

用 FileReader 读文件头魔数判断真实类型
浏览器无法直接读取文件二进制内容全量,但前 4–12 字节(魔数)足够识别绝大多数格式。这是绕过 file.type 和后缀名伪造的最轻量、最可靠手段。
常见魔数示例:
- JPEG:
FF D8 FF(3 字节) - PNG:
89 50 4E 47(4 字节) - GIF:
47 49 46 38(GIF8) - PDF:
25 50 44 46(%PDF) - XLSX:
50 4B 03 04(PK\x03\x04)
实操建议:
- 用
FileReader.readAsArrayBuffer(file)读取前若干字节,再转为Uint8Array比对 - 不要只读 1 字节——PNG 和 GIF 都以
0x89或0x47开头,单字节易冲突 - 对图片类文件,优先比 PNG/JPEG/GIF 三者;对文档类,PDF/XLSX/DOCX 魔数必须显式覆盖
- 若魔数不匹配,立即执行
event.target.value = ''清空 input,否则用户重复选同一文件时change事件不会触发
校验 HTML 文件需解析结构而非仅看魔数
HTML 不是纯二进制格式,魔数(3C 21 44 4F 43 54 59 50 45 即 <!DOCTYPE)容易被混淆或注释绕过。真正风险在于内联脚本、事件处理器和危险标签。
立即学习“前端免费学习笔记(深入)”;
实操建议:
- 先用
FileReader.readAsText(file, 'UTF-8')读取字符串,避免编码错乱导致正则失效 - 用正则粗筛高危模式:
/<script>]*>/i</script>、、<code>,但别依赖它做最终判断 - 创建临时
div,设innerHTML = text,再遍历其childNodes,检查是否存在iframe、object、embed标签,或元素是否带onclick、onload等属性 - 发现任一违规节点,停止上传并定位到具体行号(可用
text.substring(0, index).split('\n').length估算)
大文件慎用 arrayBuffer(),改用分块 + hash-wasm
调用 file.arrayBuffer() 会将整个文件加载进内存。上传 200MB 的 PDF 或视频时,页面极易卡死甚至崩溃,尤其在低端安卓设备上。
实操建议:
- 对大于 50MB 的文件,放弃全量读取,改用
FileReader分块读取前 64KB(足够覆盖所有常见魔数) - 如需 SHA-256 校验一致性,用
hash-wasm库支持流式哈希,避免crypto.subtle.digest()吃光内存 - 服务端仍要复核魔数——前端分块读只是优化体验,不能替代后端完整解析
后端才是唯一可信的魔数校验点
前端魔数校验可被完全跳过:curl 直传、禁用 JS、修改 DOM、拖拽 Drop 区域……所有前端逻辑都只是提示,不是防护。
实操建议:
- Node.js 用
file-type库(基于真实魔数,非扩展名),别信req.file.mimetype - Java Spring 中禁用
MultipartFile.getContentType(),改用Files.probeContentType(path)或 Apache Tika - PHP 必须用
finfo_file($finfo, $tmpFile),绝不能用$_FILES['file']['type'] - 存储时剥离原始文件名,重命名为 UUID + 安全扩展名(如
abc123.pdf),彻底切断后缀名信任链
魔数校验本身不难,难的是前后端对“同一份字节”的解读必须一致——前端读前 4 字节,后端也必须从文件开头读同样偏移,否则校验就失去意义。



















