必须对 contentType 进行白名单校验,因客户端完全可控,需在上传入口硬编码白名单(如 image/jpeg、image/png 等),拒绝 multipart/、application/octet-stream 等兜底类型,并结合文件头(magic bytes)二次验证,统一使用 GridFSBucket.uploadFromStream 的 options.contentType 参数写入顶层字段,建索引 db.fs.files.createIndex({ contentType: 1 }),并定期扫描清理非白名单文件。

contentType 必须白名单校验,不能只信客户端传值
客户端发来的 contentType 字段完全可控,攻击者用 curl -H "Content-Type: application/x-php" 就能绕过前端限制,把恶意脚本当“图片”上传。MongoDB 本身不校验这个字段,fs.files 集合照单全收。
实操建议:
- 在上传入口(如 Express 中间件、Spring Controller)里硬编码白名单,例如
["image/jpeg", "image/png", "application/pdf", "text/plain"] - 拒绝所有带
multipart/、application/octet-stream或空值的请求——这些是通用兜底类型,极易被滥用于伪装 - 对扩展名做二次辅助判断(如
.php、.js),但不能替代 MIME 校验,因为扩展名更易伪造 - Node.js 示例中别用
req.headers['content-type']直接取值,应从busboy或multer解析后的file.mimetype获取,它基于文件头检测,更可靠
避免在 metadata 中存 contentType,统一走标准字段
有人把 contentType 塞进 metadata 字段(如 { metadata: { contentType: "image/gif" } }),这会导致两个问题:一是无法利用 GridFSBucket 的原生字段语义(bucket.uploadFromStream() 默认写入顶层 contentType);二是后续用 find({ contentType: "image/*" }) 查询时命中不了索引——因为 MongoDB 不支持通配符索引,且 metadata.contentType 是嵌套路径,查询效率更低。
正确做法:
- 始终使用
GridFSBucket.uploadFromStream()的options.contentType参数传值,它会写入fs.files.contentType顶层字段 - 为
contentType字段建单独索引:db.fs.files.createIndex({ contentType: 1 }),便于按类型统计或清理 - 如果业务强依赖自定义类型标识(如 “internal-report-v2”),可另设
metadata.fileKind,但和标准contentType分开存、分开查
上传前校验 + 存储后复查,双保险防绕过
仅靠上传时校验不够。如果上传接口被复用(比如某 SDK 允许传任意 contentType 给内部调试用),或者有旧代码直连 fs.files.insertOne(),白名单就形同虚设。
建议补一道存储后检查:
- 每天凌晨跑一次聚合查询,扫出非白名单的
contentType:db.fs.files.aggregate([{ $match: { contentType: { $nin: ["image/jpeg", "image/png", "application/pdf"] } } }, { $project: { _id: 1, filename: 1, contentType: 1, uploadDate: 1 } }]) - 对异常文件执行
bucket.delete()并告警,不要直接删fs.files文档——否则 chunks 会变孤儿块 - 把该脚本接入 CI/CD 流水线,在部署新版本前自动检查测试库中是否存在违规
contentType
注意 multipart/form-data 请求里的 filename 和 contentType 可能不一致
浏览器上传时,Content-Type 头由文件扩展名决定,但用户可手动改 filename="shell.php" 而保持 Content-Type: image/jpeg。这时候光看头不行,得结合文件头(magic bytes)验证。
关键点:
- Node.js 可用
file-type库读取流前 4KB 判断真实类型,再与contentType对比 - Java Spring 中可用
Tika的detect()方法做类似事情,别只依赖MultipartFile.getContentType() - 校验失败时返回
415 Unsupported Media Type,而不是400,语义更准确
真正麻烦的不是写几行白名单代码,而是确保所有上传路径——包括管理后台、API 测试入口、第三方回调上传——都经过同一套校验逻辑。漏掉一个,整个类型控制就崩了。











