MongoDB注入型攻击的核心问题在于,开发者错误地认为NoSQL数据库天然免疫注入。事实恰恰相反,当应用层将用户输入直接拼接到数据库查询语句中,且未进行严格的类型校验和参数化处理时,攻击者可以通过构造特殊的JSON或JavaScript表达式来篡改查询逻辑。最常见的攻击面出现在使用$where、$regex、$gt、$ne等操作符,以及接收JSON格式查询参数的API端点中。要理解这类攻击,必须明确一点:MongoDB的查询语言本身就是一种结构化数据,如果应用程序盲目信任客户端传来的查询对象,就等于把数据库的操控权交给了攻击者。
密码验证绕过:一个典型的注入场景假设有一个登录接口,后端使用Node.js和Mongoose操作MongoDB。开发者编写了如下代码来验证用户身份:
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
if (user) {
// 登录成功
}
这种写法看似安全,但如果攻击者在用户名或密码字段中传入一个对象而非字符串,情况就会失控。例如,攻击者发送如下JSON数据:
{
"username": "admin",
"password": {"$ne": ""}
}
此时查询条件变为:password字段不等于空字符串。由于admin用户的密码确实不为空,该查询会成功匹配到admin用户,攻击者无需知道密码即可登录。这个例子揭示了MongoDB注入的本质:利用MongoDB丰富的查询操作符体系,将原本应该是字面值的参数变成了逻辑操作符。$ne、$gt、$lt、$gte、$lte、$in、$nin、$regex等操作符都可能被滥用。更危险的是,如果应用使用了$where操作符来执行JavaScript表达式,攻击者甚至可以注入任意JavaScript代码,实现类似SQL注入中堆叠查询的效果。
利用$where进行代码注入$where操作符允许在查询中执行JavaScript表达式,这为注入攻击打开了更大的后门。假设某个搜索功能允许用户输入一个条件表达式:
const results = await collection.find({
$where: "this.age > " + req.body.minAge
});
攻击者可以输入:
"0; sleep(5000); return true; //"
拼接后的表达式变成:
this.age > 0; sleep(5000); return true; //
这会导致数据库执行sleep函数,造成拒绝服务攻击。更严重的情况下,攻击者可能尝试读取服务器文件系统、建立反向Shell,或者执行任意系统命令。虽然MongoDB在较新版本中默认禁用了$where等服务器端脚本执行,但很多遗留系统仍可能开启这些功能,或者开发者为了灵活性而手动启用了javascriptEnabled选项。必须认识到,允许数据库执行用户可控的代码是极其危险的架构决策。
盲注与数据窃取当应用不直接返回数据库错误信息时,攻击者仍可通过布尔盲注或时间盲注来窃取数据。利用$regex操作符可以逐字符地猜测字段内容。例如,攻击者想获取admin用户的密码哈希,可以发送:
{
"username": "admin",
"password": {"$regex": "^a.*"}
}
如果应用返回“登录成功”,说明密码以字母a开头。接着尝试^aa.*、^ab.*,以此类推。通过自动化脚本,攻击者可以在合理时间内提取出完整密码。时间盲注则利用$where执行延时函数,根据响应时间来判断条件真假。这类攻击虽然速度较慢,但极其隐蔽,往往能绕过简单的日志监控。防御盲注的关键在于:永远不要让用户输入直接控制查询操作符,并且对所有输入进行严格的类型约束。
字段校验:第一道防线防御MongoDB注入,字段校验是成本最低、见效最快的手段。核心原则是:对所有用户输入执行严格的白名单校验,拒绝任何不符合预期类型和格式的数据。具体来说,应该从以下几个层面实施校验:
第一,类型强制转换。对于期望字符串的字段,在进入查询之前显式转换为字符串。在Node.js中可以使用String()函数或typeof检查。对于数字字段,使用Number()转换,如果结果是NaN则直接拒绝请求。对于布尔字段,严格检查是否为true或false,拒绝0、1、字符串形式的"true"等变体。这种类型层面的强制约束可以从根本上消除操作符注入的可能性,因为对象类型的{"$ne": ""}在转换为字符串时会变成"[object Object]",无法构成有效的注入载荷。
第二,结构校验。如果API设计允许客户端传递复杂的查询条件,必须对查询对象的结构进行递归校验。定义一个允许的操作符白名单,例如只允许$eq、$in等安全操作符,明确禁止$where、$regex、$ne等可能被滥用的操作符。同时限制查询对象的嵌套深度,防止攻击者构造深层嵌套的拒绝服务载荷。可以使用JSON Schema或Joi等校验库来声明式地定义允许的数据结构。
第三,内容校验。对于字符串内容,根据业务需求进行正则表达式过滤。用户名通常只允许字母数字和下划线,邮箱必须符合标准格式,手机号只允许数字和加号。对于自由文本搜索场景,对特殊字符进行转义处理,特别是$、{、}、[、]等可能在MongoDB查询语言中具有特殊含义的字符。但要注意,转义不能替代类型校验,只能作为纵深防御的一环。
参数化查询与ORM的正确使用MongoDB原生驱动和Mongoose等ODM库本身支持参数化风格的查询,关键在于开发者要正确使用。以Mongoose为例,安全的查询写法是:
const user = await User.findOne({
username: req.body.username,
password: req.body.password
});
当req.body.password是一个对象{"$ne": ""}时,Mongoose会将其视为字面值进行匹配,而不是解析为操作符。这是因为Mongoose在底层对查询值进行了类型转换和清理。但这一保护机制并非绝对可靠,不同版本的Mongoose行为可能存在差异,而且在使用某些高级查询方法时保护会减弱。因此,不能完全依赖ODM的内置保护,必须在应用层进行显式的类型校验。
对于必须使用复杂查询条件的场景,应该采用白名单模式构建查询对象。例如,一个允许用户按价格范围筛选商品的接口,后端代码应该这样写:
const query = {};
if (req.body.minPrice !== undefined) {
const minPrice = Number(req.body.minPrice);
if (!isNaN(minPrice)) {
query.price = { ...query.price, $gte: minPrice };
}
}
if (req.body.maxPrice !== undefined) {
const maxPrice = Number(req.body.maxPrice);
if (!isNaN(maxPrice)) {
query.price = { ...query.price, $lte: maxPrice };
}
}
const products = await Product.find(query);
这种方式下,操作符由后端代码硬编码,用户只能控制数值部分,无法注入额外的操作符。查询结构的控制权牢牢掌握在开发者手中,攻击者无论传入什么数据都无法改变查询逻辑。
深度防御:数据库层安全配置除了应用层校验,MongoDB本身提供了多项安全配置来降低注入攻击的风险。首先,禁用服务器端JavaScript执行。在mongod.conf配置文件中设置:
security: javascriptEnabled: false
或者在启动时添加--noscripting参数。这会直接禁用$where、mapReduce、group等需要执行JavaScript的功能。如果业务确实不需要这些功能,这是最彻底的保护措施。但要注意,某些聚合管道阶段也依赖JavaScript,禁用前需要评估业务影响。
其次,启用查询操作符限制。MongoDB 5.0及以上版本支持通过设置operatorWhitelist来限制可用的查询操作符。可以在集合级别或数据库级别配置,只允许业务必需的查询操作符。例如,如果业务只需要等值查询和范围查询,可以只开放$eq、$gt、$gte、$lt、$lte、$in,禁用$where、$regex、$expr、$function等高风险操作符。
第三,实施最小权限原则。应用程序连接数据库的账号应该只拥有执行其业务逻辑所需的最小权限。如果应用只需要对特定集合进行增删改查,就不要授予数据库管理权限、不要授予对其他集合的访问权限、不要授予执行eval的权限。即使攻击者成功注入了恶意代码,权限限制也能大幅降低损害程度。定期审计数据库账号权限,确保没有过度授权的情况。
第四,启用审计日志并设置告警。MongoDB Enterprise版提供了审计功能,可以记录所有数据库操作。配置审计规则,重点关注包含$where、$regex等敏感操作符的查询,以及来自异常IP或异常时间段的请求。结合SIEM系统,当检测到可疑的查询模式时自动触发告警,使安全团队能够快速响应潜在的攻击行为。
输入校验的工程化实践将字段校验融入开发流程,需要建立一套可复用的校验中间件或工具函数库。在Node.js生态中,Joi、express-validator、class-validator等库可以很好地承担这一职责。以express-validator为例,一个完整的校验链如下:
const { body, validationResult } = require('express-validator');
app.post('/login', [
body('username')
.isString()
.trim()
.isLength({ min: 3, max: 30 })
.matches(/^[a-zA-Z0-9_]+$/),
body('password')
.isString()
.isLength({ min: 8, max: 128 }),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 校验通过后,显式提取字符串值
const username = String(req.body.username);
const password = String(req.body.password);
// 继续业务逻辑...
next();
}
]);
这段代码的关键点在于:先通过校验库确保输入符合预期格式,然后显式地使用String()进行类型转换,最后才将变量传入数据库查询。即使攻击者尝试传入对象、数组或其他类型,校验阶段就会直接返回400错误,根本不会进入数据库查询环节。这种“校验-转换-使用”的三步模式应该成为所有API端点的标准做法。
对于GraphQL API,同样需要在Schema层面定义严格的标量类型。GraphQL的类型系统天然提供了一层保护,但自定义标量或JSON类型仍然可能引入注入风险。建议避免使用通用的JSON标量类型接收客户端查询条件,而是为每个查询定义明确的输入类型,将可接受的参数结构限定在最小范围内。
测试与监控安全措施的有效性需要通过测试来验证。在CI/CD流水线中集成安全测试用例,模拟各种注入载荷。测试用例应覆盖:对象型载荷如{"$ne": ""}、数组型载荷、嵌套对象载荷、包含$where的字符串载荷、超长字符串、特殊Unicode字符等。使用自动化安全扫描工具如OWASP ZAP或自定义的模糊测试脚本,定期对API进行扫描。同时,在生产环境中监控异常查询模式,例如短时间内大量包含$regex的请求、来自单个IP的递增式正则匹配请求(盲注特征)、响应时间异常波动等。这些监控指标能够帮助安全团队在攻击造成实际损害之前发现并阻断威胁。
MongoDB注入型攻击并非无解难题,但需要开发者摒弃“NoSQL就安全”的错误认知,将输入校验提升到架构设计的高度来对待。类型约束、结构白名单、最小权限、禁用危险功能,这四道防线相互配合,能够将注入风险降至可控范围。安全是一个持续的过程,随着MongoDB版本的演进和攻击技术的更新,防御策略也需要不断调整和加固。
