NoSQL 注入怎么发生
文档数据库上的注入典型形态是「查询条件被整体反序列化」:后端把客户端提交的 JSON 直接当查询对象传给 MongoDB,攻击者提交 password 为 {"$ne":null} 这类带操作符的值,就能改写条件逻辑、绕过登录校验,或通过 $regex、$where 扩大查询范围。查询操作符本身是正常功能,问题出在用户输入能决定查询结构。
基本信息
| 主要目标 | MongoDB 及其 ODM(如 mongoose) |
|---|---|
| 注入载体 | 请求体 JSON、查询串、数组参数 |
| 危险操作符 | $ne、$gt、$in、$regex、$where、$function |
| 可能后果 | 认证绕过、越权查询、正则拒绝服务 |
| 修复方向 | 强制类型转换、字段白名单、集合 schema 校验 |
| 辅助收敛 | 禁用服务端 JavaScript,关闭或限制 $where |
| 覆盖范围 | 所有把用户输入当查询条件的接口,不只登录 |
风险写法与安全写法
| 场景 | 风险写法 | 安全写法 |
|---|---|---|
| 登录校验 | findOne(req.body) | 显式取字段并做 String 转换与长度校验 |
| 列表筛选 | 把 query 整体传入 find() | 只接受白名单内的筛选项 |
| 关键字搜索 | 用户输入直接进 $regex | 转义正则元字符并限制长度 |
| 聚合统计 | 用户输入拼进 $where | 禁用服务端脚本,改用普通查询实现 |
| 排序分页 | 用户传入 sort 对象 | 映射到固定字段名与固定方向 |
风险写法与修复
风险写法与修复
// 风险:直接使用客户端 JSON 作为查询条件
// db.collection('users').findOne(req.body)
// 修复:只取所需字段并强制类型
const username = String(req.body.username || '');
const password = String(req.body.password || '');
if (!username || username.length > 64) throw new Error('invalid input');
db.collection('users').findOne({ username, password });输入层限制(示意)
输入层限制(示意)
拒绝非字符串/数字类型的参数:对象与数组一律拒绝
过滤以 $ 开头的键名,禁止出现在用户可控的字段名里
为集合定义 schema 校验($jsonSchema 或 ODM schema)限制字段类型
禁用服务端 JavaScript 执行,避免 $where、$function 被滥用
正则搜索先转义元字符并限制长度,防止 ReDoS典型使用场景
- 新增登录、搜索、导出接口时的代码评审要点
- 渗透测试报告的整改项复核
- 从裸驱动迁移到 ODM 时,检查类型校验是否真的生效
- 老系统加固:禁用服务端脚本、收敛查询字段
- 日志排查:发现带操作符的用户输入时定位到接口
- 安全编码培训中作为对象注入的代表案例
ODM 提供 schema 校验时,为集合定义严格 schema 可显著降低此类风险,但校验规则要覆盖全部写入路径,包括脚本导入与数据修复作业。
只在登录接口做类型校验不够:搜索、筛选、导出接口同样可能把用户输入当查询对象,等于留了另一个入口。