Security advisory GHSA-m7xq-9374-9rvx: Mongoose search injection vulnerability
Package: mongoose (npm), vulnerable version: 8.8.2, fixed in: 8.8.3
CWE: CWE-89
Files changed by the fix:
lib/helpers/populate/assignVals.js
lib/helpers/populate/getModelsMapForPopulate.js

Fix diff:
diff --git a/lib/helpers/populate/assignVals.js b/lib/helpers/populate/assignVals.js
index 92f0ebecd05..babdf67716d 100644
--- a/lib/helpers/populate/assignVals.js
+++ b/lib/helpers/populate/assignVals.js
@@ -243,7 +243,7 @@ function numDocs(v) {
 
 function valueFilter(val, assignmentOpts, populateOptions, allIds) {
   const userSpecifiedTransform = typeof populateOptions.transform === 'function';
-  const transform = userSpecifiedTransform ? populateOptions.transform : noop;
+  const transform = userSpecifiedTransform ? populateOptions.transform : v => v;
   if (Array.isArray(val)) {
     // find logic
     const ret = [];
@@ -335,7 +335,3 @@ function isPopulatedObject(obj) {
     obj.$__ != null ||
     leanPopulateMap.has(obj);
 }
-
-function noop(v) {
-  return v;
-}
diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js
index 8ae89fcbbd3..83aaf981d57 100644
--- a/lib/helpers/populate/getModelsMapForPopulate.js
+++ b/lib/helpers/populate/getModelsMapForPopulate.js
@@ -182,6 +182,15 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
     if (hasMatchFunction) {
       match = match.call(doc, doc);
     }
+    if (Array.isArray(match)) {
+      for (const item of match) {
+        if (item != null && item.$where) {
+          throw new MongooseError('Cannot use $where filter with populate() match');
+        }
+      }
+    } else if (match != null && match.$where != null) {
+      throw new MongooseError('Cannot use $where filter with populate() match');
+    }
     data.match = match;
     data.hasMatchFunction = hasMatchFunction;
     data.isRefPath = isRefPath;
@@ -460,6 +469,16 @@ function _virtualPopulate(model, docs, options, _virtualRes) {
     data.match = match;
     data.hasMatchFunction = hasMatchFunction;
 
+    if (Array.isArray(match)) {
+      for (const item of match) {
+        if (item != null && item.$where) {
+          throw new MongooseError('Cannot use $where filter with populate() match');
+        }
+      }
+    } else if (match != null && match.$where != null) {
+      throw new MongooseError('Cannot use $where filter with populate() match');
+    }
+
     // Get local fields
     const ret = _getLocalFieldValues(doc, localField, model, options, virtual);
 
diff --git a/test/model.populate.test.js b/test/model.populate.test.js
index 78a6c2108bb..42be868b641 100644
--- a/test/model.populate.test.js
+++ b/test/model.populate.test.js
@@ -4166,6 +4166,52 @@ describe('model: populate:', function() {
         assert.deepEqual(band.members.map(b => b.name).sort(), ['AA', 'AB']);
       });
 
+      it('match prevents using $where', async function() {
+        const ParentSchema = new Schema({
+          name: String,
+          child: {
+            type: mongoose.Schema.Types.ObjectId,
+            ref: 'Child'
+          },
+          children: [{
+            type: mongoose.Schema.Types.ObjectId,
+            ref: 'Child'
+          }]
+        });
+
+        const ChildSchema = new Schema({
+          name: String
+        });
+        ChildSchema.virtual('parent', {
+          ref: 'Parent',
+          localField: '_id',
+          foreignField: 'parent'
+        });
+
+        const Parent = db.model('Parent', ParentSchema);
+        const Child = db.model('Child', ChildSchema);
+
+        const child = await Child.create({ name: 'Luke' });
+        const parent = await Parent.create({ name: 'Anakin', child: child._id });
+
+        await assert.rejects(
+          () => Parent.findOne().populate({ path: 'child', match: { $where: 'console.log("oops!");' } }),
+          /Cannot use \$where filter with populate\(\) match/
+        );
+        await assert.rejects(
+          () => Parent.find().populate({ path: 'child', match: { $where: 'console.log("oops!");' } }),
+          /Cannot use \$where filter with populate\(\) match/
+        );
+        await assert.rejects(
+          () => parent.populate({ path: 'child', match: { $where: 'console.log("oops!");' } }),
+          /Cannot use \$where filter with populate\(\) match/
+        );
+        await assert.rejects(
+          () => Child.find().populate({ path: 'parent', match: { $where: 'console.log("oops!");' } }),
+          /Cannot use \$where filter with populate\(\) match/
+        );
+      });
+
       it('multiple source docs', function(done) {
         const PersonSchema = new Schema({
           name: String,