Node 服务怎样安全地接收文件上传?
先理解原理和示例,再练习完整口述与追问。
回答要点
上传使用 multipart/form-data,服务端用 multer、busboy、formidable 或 koa-body 流式解析,限制文件大小、数量和字段长度;用文件内容的魔数判断真实类型,而不是只信扩展名和 Content-Type;文件名由服务端生成,存储在 Web 根目录之外或对象存储,下载时设置正确的 Content-Type 与 Content-Disposition。大文件用分片和断点续传。
原理拆解
multipart 请求体按 boundary 分隔多个部分,每部分有自己的头部(name、filename、Content-Type)。流式解析可以边接收边写入磁盘或转发到对象存储,避免把整个文件放进内存。koa-body 内部使用 formidable,需开启 multipart: true;Express 常用 multer,配置 limits 与 fileFilter。
客户端声明的 MIME 类型可以随意伪造,应读取文件头部的魔数:PNG 以 89 50 4E 47 开头,JPEG 以 FF D8 FF 开头,PDF 以 %PDF 开头。SVG、HTML 等可执行脚本的文件要么拒绝,要么在独立域名下以附件形式提供。原始文件名可能包含 ../ 路径穿越或特殊字符,只作为元数据保存。
示例与推演
import multer from 'multer'
const upload = multer({
dest: '/data/uploads',
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
fileFilter(req, file, callback) {
callback(null, ['image/png', 'image/jpeg'].includes(file.mimetype))
}
})
app.post('/avatar', requireLogin, upload.single('avatar'), async (req, res) => {
const head = await readFirstBytes(req.file.path, 4)
if (!isPng(head) && !isJpeg(head)) { await removeFile(req.file.path); return res.status(400).end() }
res.json({ id: req.file.filename })
})
fileFilter 中的 mimetype 只是第一道过滤,魔数校验才是关键。大文件上传可由服务端签发预签名 URL,让浏览器直传对象存储,服务端不承担流量。
易错点
不设置大小限制会被大文件拖垮磁盘和内存;上传目录可执行脚本会造成远程代码执行;图片还应考虑重新编码以去除 EXIF 地理位置等隐私信息。
参考回答可直接练习口述
上传用 multipart/form-data,服务端用 multer、busboy 或 koa-body 流式解析,限制大小、数量和字段。类型校验不能只看扩展名和 Content-Type,要读文件头魔数;文件名由服务端生成,原名只做元数据,防止路径穿越。存储放在 Web 根目录外或对象存储,SVG、HTML 这类可能含脚本的文件要拒绝或用独立域名附件下载。大文件用分片续传或预签名 URL 直传对象存储。