accept属性仅控制文件选择对话框默认过滤,无法阻止非法文件上传;可靠做法是双写MIME与扩展名(如"application/pdf,.pdf"),前端用文件名后缀+magic bytes校验,后端必须基于二进制流二次校验。

accept 属性根本拦不住非法文件
它只控制文件选择对话框默认显示哪些类型,用户点“所有文件”、拖拽、粘贴、甚至删掉 accept 属性,都能上传任意后缀。Chrome 里选个 .exe 改名为 report.pdf,accept=".pdf" 完全没反应——这不是 bug,是设计如此。
常见错误现象包括:accept="image/*" 下用户仍传了 shell.php;iOS Safari 可能直接跳过该属性;华为 EMUI 和小米 MIUI 的 WebView 常忽略它;设了 multiple 后用户一次勾选 .pdf 和 .zip,event.target.files 里两个都在。
accept 值怎么写才在主流浏览器里真生效
单靠 MIME 类型或单靠扩展名都容易失效。必须双写,且逗号后**不能有空格**:
-
accept="application/pdf,.pdf"✅ 最小可靠组合,覆盖 Chrome/Safari/iOS -
accept="application/vnd.openxmlformats-officedocument.wordprocessingml.document,.docx"✅ .docx 必须带完整 MIME -
accept="image/png,image/jpeg,.png,.jpg,.jpeg"✅ 显式列出变体,避免image/pjpeg被漏 -
accept="text/plain,.csv"✅ 无空格,旧版 Safari 才不丢整个属性 -
accept="image/*,.png"❌ Safari 可能丢掉.png -
accept=".pdf, .docx"❌ 空格导致 Safari 解析失败
前端 JS 校验该信 file.name 还是 file.type
两个都不全信。file.type 是浏览器推测值,可为空、可伪造;file.name 可被手动构造。真正轻量可靠的校验要分两步:
立即学习“前端免费学习笔记(深入)”;
- 先用
file.name.toLowerCase().endsWith('.pdf')快速过滤明显违规扩展名 - 再用
FileReader读取前 4 字节比对 magic bytes:new Uint8Array(await file.slice(0, 4).arrayBuffer()) - Pdf 头必须是
[0x25, 0x50, 0x44, 0x46](即%PDF),PNG 是[0x89, 0x50, 0x4E, 0x47] - 必须遍历
event.target.files每一项,不能只检查第一个 - 校验失败时立刻执行
e.target.value = "",别等 submit 触发
后端才是唯一可信边界
所有前端限制都可被绕过:curl、Postman、DevTools 修改 DOM、拖拽 Drop 区域……服务端收到的 Content-Type 请求头、req.file.mimetype(Node.js)或 $_FILES['file']['type'](PHP)全不可信。
真实校验必须基于二进制流:
- 用
file-type(Node.js)或python-magic(Python)读取文件头,获取真实 MIME - 检查扩展名是否在白名单中,且与真实 MIME 逻辑一致(例如
.jpg应对应image/jpeg,而非text/html) - 对
.html、.js、.zip等高危类型,即使 MIME 正确也要额外拦截 - 存储时剥离原始扩展名,用服务端生成的随机名 + 白名单后缀
最容易被忽略的是:哪怕前端 JS 已校验 magic bytes,后端仍必须重做一遍——因为文件可能在传输中被篡改,或前端校验被跳过。



















