accept 属性仅提示系统文件选择器默认过滤项,无法真正限制上传,真正有效的校验必须依赖前端 change 事件中的 js 检查(扩展名+magic bytes)与后端二进制流解析(忽略客户端 content-type 和扩展名,重验 mime 并安全重命名)。

accept 属性确实能影响系统文件选择器的默认过滤项,但它不是“限制”,只是提示——用户点“所有文件”、拖拽、或删掉 accept 后再选,都能绕过。真正起作用的只有后端校验和前端 change 事件里的 JS 检查。
accept=".pdf,.xlsx" 写法看似直观,但兼容性差
很多开发者直接写 accept=".pdf,.xlsx",结果在 iOS Safari 上完全失效,连相册入口都消失;旧版 Safari 遇到逗号后带空格(如 accept="image/png , .jpg")会直接丢弃整个属性。更稳妥的做法是:
- 优先用标准 MIME 类型:
accept="application/pdf,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - 扩展名写法仅作补充,且必须加点号、小写、无空格:
accept=".pdf,.xlsx"(不能写成pdf或.PDF) - 混合使用时注意顺序:MIME 在前,扩展名在后,比如
accept="image/png,image/jpeg,.png,.jpg" - 避免通配符混搭:不要写
accept="image/*,.pdf",image/*会覆盖后续精确匹配逻辑
为什么 file.type 经常为空,而扩展名检查也不可靠
file.type 是浏览器根据文件扩展名或系统注册表推测的,上传时可被任意伪造;扩展名本身也能被手动改名绕过(比如把 hack.html 改成 report.pdf)。所以实际校验必须分两步:
- 先做扩展名白名单:用
file.name.toLowerCase().endsWith('.pdf')或正则/\.(pdf|xlsx|docx)$/i - 关键业务必须加 magic bytes 校验:读取文件前 4 字节,比对真实格式签名,例如
%PDF(PDF)、PK\x03\x04(ZIP/XLSX)、\x89PNG(PNG) - 如果用了
multiple,记得遍历Array.from(event.target.files),不能只看第一个
按钮触发 click() 时,accept 还生效吗
生效,但和谁触发无关。accept 是绑定在 <input type="file"> 元素上的,不管用户点原生浏览按钮,还是你用 JS 调用 input.click(),弹出的系统对话框行为完全一致。它不会因为“按钮是自定义的”就失效,也不会因此增强或减弱过滤能力——它从来就不是过滤器,只是界面提示。
真正容易被忽略的是:用户在点击按钮后,你没有任何机会干预文件选择过程;唯一可控的节点是 change 事件触发之后。这时候文件已经选完,JS 才能读取 event.target.files 并决定是否清空输入框、提示错误、或阻止后续提交。
后端才是唯一可信的防线
前端所有校验,包括 accept、扩展名比对、甚至 magic bytes 检查,在服务端眼里都形同虚设。攻击者可以:
- 用
curl直传伪造的Content-Type和文件名 - 用 Postman 修改请求头,跳过所有前端逻辑
- 在 DevTools 中临时删除
accept属性后手动选择
所以服务端必须:
- 忽略客户端传来的
Content-Type和文件扩展名 - 读取文件二进制流前若干字节,用
file-type(Node.js)、python-magic(Python)等库解析真实 MIME - 保存时剥离原始后缀,由服务端根据检测结果附加安全扩展名(如
upload_abc123.pdf→upload_abc123_1234567890.pdf) - 上传目录禁止执行权限,静态资源走独立域名或 CDN
最常被忽略的一点:即使你前端做了完整的 magic bytes 校验,只要没在服务端重做一遍,这个上传流程就不算安全。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











