MongoDB本身不解析字符串查询,故无传统SQL注入,但用户输入若被当作BSON操作符执行(如{"$ne": ""}),将导致NoSQL注入;Schema Validation仅对写操作生效,无法防御find类查询攻击,必须在应用层显式校验、剥离或拒绝以$开头的操作符。

MongoDB本身不解析字符串查询,所以不存在传统SQL注入,但“SQL注入风格的逻辑攻击”真实存在——本质是用户输入被当作BSON操作符执行,比如 {"$ne": ""} 或 {"$regex": ".*"}。强制开启Schema Validation(模式校验)能拦截部分非法结构,但它不是银弹,仅对插入/更新生效,对 find、findOne 这类查询完全无效。
Schema Validation 只管写入,不管查询
Validation 是 MongoDB 在 insertOne、updateOne 等写操作时触发的校验机制,靠 validator 规则限制字段类型、格式、取值范围。但它对读操作(find、count、aggregate)零干预——攻击者传 {"username": {"$ne": null}} 给 findOne(),Validation 压根不会碰这个请求。
- 验证只在 collection 级别配置,需显式启用:
validationLevel: "strict"+validationAction: "error" - 规则写在
validator字段里,例如限制username必须是字符串且非空:{ username: { $type: "string", $minLength: 1 } } - 它防不住
Object.assign({}, req.body, { status: "active" })这类后端合并污染,因为合并发生在数据库层之前
真正该拦在查询前的操作符黑名单
所有以 $ 开头的键(如 $ne、$regex、$where、$expr)都可能被用户输入带入查询对象,必须在进入数据库前剥离或拒绝。
- 最直接的做法:遍历请求体所有嵌套层级,检测是否存在
key.startsWith("$"),一旦发现立即拒收(HTTP 400) - 禁用高危操作符:尤其
$where(执行任意 JS)、$regex(若未转义且无索引前缀,性能爆炸) - 不要依赖
typeof req.body.username === "string"—— 攻击者用application/jsonContent-Type 就能传对象,Node.js 默认解析为原生 JS 对象
Mongoose 的 strictQuery 不是安全开关
Mongoose v7+ 默认开启 strictQuery: true,但它只影响“未知字段”是否被忽略,**完全不拦截操作符**。比如你定义 Schema 为 { username: String },攻击者仍可传 { username: { $ne: null } },Mongoose 照样把这整个对象交给 MongoDB。
-
strictQuery: true的实际行为:遇到 Schema 里没定义的字段(如admin: true),直接丢弃;但$ne是 MongoDB 内置操作符,不是“自定义字段” - 真正起作用的是
sanitizeFilter插件或手动递归清洗函数,不是框架默认行为 - 即使用了 Mongoose,也必须在
find()前做JSON.stringify(req.query)检查或白名单字段提取
绕过 Validation 的典型路径:原型链污染 & 合并污染
Validation 再严,也拦不住从 JavaScript 层就污染了查询对象的攻击。常见手法包括利用 Object.assign()、lodash.merge() 或 Express 的 req.body 自动解析机制,把恶意操作符塞进深层对象。
- 前端悄悄加
__proto__.username,某些老版本 Express 会将其解析为{ username: { $ne: null } } -
const query = Object.assign({ status: "active" }, req.query)—— 若req.query含{"$or": [...]},直接污染最终查询 - 防御关键点:永远用白名单构造查询对象,而不是“合并+过滤”,例如:
{ username: sanitizeString(req.query.username), category: req.query.category || "all" }
最易被忽略的一点:Validation 和查询过滤是两层事,前者管“数据长什么样”,后者管“查询怎么写”。很多团队配完 validator 就以为高枕无忧,结果登录接口仍被 {"password": {"$ne": null}} 一发破防——因为漏掉了查询层的结构净化。


















