聚合管道注入是逻辑篡改漏洞,因用户输入未经校验直接拼入$match、$group等BSON阶段参数所致,攻击可藏于$project、$addFields或$facet子管道,须禁止用户控制$开头键名并做类型+范围双重校验。

聚合管道注入不是数据库漏洞,而是把用户输入直接拼进 $match、$group 等阶段对象时引发的逻辑篡改——和查询注入本质相同,但攻击面更隐蔽。
为什么 db.collection.aggregate() 会中招
聚合管道接受任意 BSON 对象作为阶段参数,而攻击者可提交 {"$ne": ""}、{"$gt": 0} 甚至嵌套的 {"$expr": {"$regexMatch": {...}}}。一旦后端不做校验就原样塞进 pipeline 数组,整个聚合逻辑就被绕过或扭曲。
常见错误现象:
- 搜索接口输
{"$regex": ".*"}返回全量用户 - 分页参数
skip被替换成{"$where": "sleep(1000)"}(若启用了 JS 引擎) -
$group阶段的_id字段被替换为{"$concat": ["$email", "@fake"]},导致聚合结果错乱
关键点:MongoDB 不区分“用户输入”和“开发写死的逻辑”,只要 BSON 合法,它就执行。
对每个 pipeline 阶段做字段白名单校验
不能只校验第一层 $match,因为攻击可能藏在 $project 的表达式里、$addFields 的计算字段中,甚至 $facet 的子管道内。
实操建议:
- 禁止用户控制任何以
$开头的键名,如req.body.stage不能是"$group"或"$lookup" - 对 stage 内部字段做类型+范围双重检查:例如
skip必须是typeof === 'number'且>= 0 && - 用白名单限定允许的操作符:
["eq", "gt", "lt", "in"]→ 映射为{"$eq": ...},而非放任用户传"$ne"或"$regex" - 对字符串类字段(如
$project中的字段路径)限制长度和字符集:/^[a-zA-Z0-9._$]{1,64}$/,拒绝"$eval"、"$function"等高危路径
禁用或严格隔离高危聚合操作符
$where、$expr、$function、$accumulator 这些阶段自带 JS 执行能力或任意表达式解析,是聚合注入的“特权通道”。哪怕前面加了类型检查,只要它们接收原始输入,防御就失效。
实操建议:
- 生产环境彻底关闭
javascriptEnabled: false(WiredTiger 默认关闭,但需确认配置) - 若必须用
$regex,只允许出现在$match阶段,且必须配合$options: "i"和转义:req.query.q.replace(/[.*+?^${}()|[]\]/g, '\$&') - 避免在
$group的_id中使用字段路径表达式以外的内容;{"_id": "$user.email"}安全,{"_id": {"$concat": ["$user.email", "@x"]}}危险 - 对
$lookup的pipeline字段做递归白名单校验——它内部又是一个完整聚合管道
用 $literal 和显式构造替代动态拼接
不要用字符串模板或 Object.assign() 拼 pipeline,而是把用户输入当作纯数据,由代码显式构造合法阶段对象。
示例对比:
❌ 危险写法
const unsafePipeline = [
{ $match: { status: req.query.status } }
];
✅ 安全写法
const status = req.query.status;
const matchStage = {};
if (status === 'active') matchStage.status = 'active';
else if (status === 'inactive') matchStage.status = 'inactive';
// 其他分支...
const safePipeline = [{ $match: matchStage }];
更进一步,对所有字段路径表达式强制包裹 $literal:
// 用户想按固定字符串分组,不是按字段值
{ $group: { _id: { $literal: "static_key" }, count: { $sum: 1 } } }
容易忽略的一点:聚合管道中的 $$CURRENT、$$ROOT 等系统变量虽不直接受控,但若用户能影响 $project 输出结构,就可能间接操控后续阶段的 $$CURRENT 内容——防御必须贯穿整个 pipeline 生命周期。

















