首页 / 帮助文档 / MongoDB字段验证预防注入式查询攻击

MongoDB字段验证预防注入式查询攻击

MongoDB的注入式查询攻击,和传统SQL注入的逻辑完全不同。攻击者不会去尝试闭合引号,而是利用MongoDB查询语言本身的特性,通过传入特定的数据类型或操作符,来篡改原本的查询逻辑。比如,一个本该接收字符串的字段,攻击者传入一个对象,对象里包含$gt、$ne甚至$where这类操作符,就能绕过程序员的预期,读取全量数据或者执行服务端代码。解决这个问题的核心思路,不是去转义字符,而是要在数据进入查询引擎之前,对字段的值和类型进行严格的校验与清洗。下面直接拆解具体的攻击手法和对应的防御方案。

攻击者如何利用字段类型注入操作符

假设后端Node.js代码接收前端传来的用户名和密码,构造查询语句如下:

db.collection('users').findOne({
  username: req.body.username,
  password: req.body.password
})

正常请求体是这样的:

{
  "username": "admin",
  "password": "123456"
}

但攻击者可以发送:

{
  "username": "admin",
  "password": { "$ne": "" }
}

这时MongoDB实际执行的查询变成了:

db.collection('users').findOne({
  username: "admin",
  password: { $ne: "" }
})

$ne代表“不等于”,这意味着只要admin用户的密码不是空字符串,这个查询就会返回结果,攻击者直接绕过了密码验证。更危险的是,攻击者还可以传入$regex来进行盲注,或者利用$where执行任意JavaScript代码。如果应用没有对字段类型做限制,这些操作符就会被MongoDB原生解析执行。

根本原因:信任了用户输入的数据类型

大部分开发者只检查了字段是否存在,或者值的长度是否合法,却忽略了值的类型。在JavaScript中,typeof操作很容易被绕过,因为HTTP请求体解析后,数字、布尔值、对象都会被原样保留。MongoDB的驱动不会区分这个对象是程序员写的查询条件,还是用户传入的脏数据。一旦对象里携带了操作符键名,查询逻辑就被污染了。这不是MongoDB的漏洞,而是应用层缺乏类型契约导致的。

第一道防线:使用mongo-sanitize或类似工具做深度清理

最直接的防御手段是在数据进入查询前,把所有以美元符号$开头的键名从对象中剔除。这类操作不能只做浅层删除,必须递归处理嵌套对象。可以自己写递归函数,也可以直接使用社区成熟的npm包mongo-sanitize。核心逻辑如下:

function sanitize(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  if (Array.isArray(obj)) return obj.map(sanitize);
  const cleaned = {};
  for (let key in obj) {
    if (key.startsWith('$')) continue;
    cleaned[key] = sanitize(obj[key]);
  }
  return cleaned;
}

在路由处理中,对所有来自req.body、req.query、req.params的数据都调用一次sanitize函数。这样即便攻击者传入了$gt或$ne,这些键也会被直接丢弃,查询条件变回普通的值比较。但要注意,这种清理方式会改变输入数据的结构,需要在日志中记录原始请求体,方便后续安全审计。

第二道防线:使用JSON Schema做严格类型校验

清理操作符只是兜底策略,更严谨的做法是定义每个字段期望的数据类型,拒绝任何不符合类型契约的输入。JSON Schema是描述JSON数据结构的标准,Node.js生态中有ajv这样的高性能校验库。针对登录接口,可以定义如下Schema:

const schema = {
  type: "object",
  properties: {
    username: { type: "string", minLength: 1, maxLength: 50 },
    password: { type: "string", minLength: 6, maxLength: 128 }
  },
  required: ["username", "password"],
  additionalProperties: false
};

当攻击者传入password字段为对象时,ajv会直接校验失败,返回400错误,根本不会执行到数据库查询。additionalProperties设为false还能防止攻击者注入多余的查询字段。这种白名单模式比黑名单清理更可靠,因为它从根本上限制了数据的形态。

第三道防线:使用Mongoose的Schema类型约束

如果你使用Mongoose作为ODM,它本身就提供了字段类型定义功能。在定义Model时,明确指定每个字段的类型,Mongoose会在保存或查询前进行类型转换和校验。例如:

const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true }
});

当传入password为对象时,Mongoose会尝试将其转换为字符串,转换失败则抛出CastError。不过需要注意的是,Mongoose的类型转换机制在某些边界情况下可能被绕过,比如传入一个带有toString方法的对象。因此不能完全依赖Mongoose的类型系统,最好结合mongo-sanitize一起使用。

针对$where和JavaScript代码注入的专项防御

$where操作符允许在查询中执行JavaScript表达式,这是MongoDB中最危险的特性之一。攻击者如果能够控制$where的参数,就能执行任意服务端代码。防御策略非常明确:在生产环境中,通过MongoDB服务端配置禁用JavaScript执行。在mongod.conf中设置:

security:
  javascriptEnabled: false

如果业务必须使用$where,那么绝对不允许用户输入直接拼接到$where表达式中。应该使用参数化方式,将用户输入作为变量传入,并且对变量做严格的白名单校验。但更建议的做法是重新设计查询逻辑,用聚合管道或$expr替代$where,从根源上消除代码注入的风险。

聚合管道中的注入风险与防御

聚合管道的$match、$project、$group等阶段同样可能被注入操作符。攻击者如果能够控制聚合管道的某个阶段对象,就能修改整个管道的逻辑。防御方式与普通查询一致:对所有用户输入做sanitize处理,并用JSON Schema校验聚合管道中使用的变量。特别要注意$lookup和$merge这类能跨集合操作的阶段,它们可能被利用来读取或写入敏感数据。

实际案例:电商系统价格过滤接口的注入漏洞

某电商平台有一个商品列表接口,接收前端传来的价格范围过滤条件:

{
  "price": { "$gte": 100, "$lte": 500 }
}

后端直接把这个对象放入find查询的过滤条件中。攻击者发现后,尝试传入:

{
  "price": { "$regex": ".*" }
}

由于price字段是数字类型,$regex操作符导致MongoDB报错,暴露了数据库字段类型信息。攻击者进一步利用这个报错信息,结合其他接口进行联合查询注入。正确的做法是,后端不应该直接信任前端传来的查询操作符结构,而是定义好允许的操作符白名单,例如只允许$gte和$lte,其他操作符一律过滤。同时,对price的值做类型校验,确保必须是数字。

多层防御架构的搭建顺序

第一层:在网关或中间件层,对所有入站请求体做递归的$操作符清理,这是最外层的兜底。第二层:在路由处理层,使用JSON Schema或Joi对请求体做严格校验,拒绝任何不符合预期的类型和结构。第三层:在数据访问层,使用Mongoose的Schema类型定义,再次确保写入查询的数据类型正确。第四层:在MongoDB服务端,禁用JavaScript执行,限制聚合管道的内存使用和执行时间。这四层防御任何一层单独使用都可能被绕过,但组合起来能极大提升攻击成本。

日志监控与异常检测

即使做好了字段验证,也要假设防御可能被突破。需要在应用层记录所有MongoDB查询的原始输入,特别是那些触发了类型错误或操作符清理的请求。可以设置告警规则,当某个IP在短时间内产生大量sanitize清理日志时,说明有人在尝试注入攻击。MongoDB自带的慢查询日志和currentOp命令也能帮助发现异常的查询模式,比如大量使用$where或全表扫描的查询。

总结操作清单

1. 对所有用户输入递归删除以$开头的键名;

2. 使用JSON Schema或Joi定义每个接口的入参类型,拒绝对象类型的值传入字符串字段;

3. 使用Mongoose时明确每个字段的type,不要使用Mixed类型;

4. 禁用MongoDB服务端的JavaScript执行;

5. 聚合管道中的变量同样需要sanitize和类型校验;

6. 记录所有类型校验失败的请求日志,建立异常监控;

7. 定期进行代码审查,检查是否有直接使用req.body作为查询条件的代码。这套组合方案能有效防御绝大多数MongoDB注入式查询攻击,且对业务侵入性小,性能损耗几乎可以忽略。