CVE-2024-53900
Description
Mongoose before 8.8.3 fails to sanitize $where in match conditions, enabling search injection attacks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Mongoose before 8.8.3 fails to sanitize $where in match conditions, enabling search injection attacks.
Vulnerability
Description Mongoose versions before 8.8.3 contain a search injection vulnerability related to the improper use of the $where operator in match conditions [1]. This flaw allows an attacker to inject arbitrary JavaScript expressions into database queries, bypassing the intended query logic. The root cause is insufficient input validation when handling the $where clause within MongoDB aggregation pipelines or find operations.
Exploitation
To exploit this vulnerability, an attacker must be able to supply untrusted input that is used in a match stage or similar query context. No special privileges are required beyond the ability to provide crafted data to an application using Mongoose. The attacker can inject a $where clause containing arbitrary JavaScript, which will be executed by the MongoDB server as part of the query [2].
Impact
Successful exploitation can lead to unauthorized data access, retrieval of sensitive information, or bypassing security controls. Since $where allows server-side JavaScript execution, it may also enable more severe attacks depending on the database configuration, though the primary impact is information disclosure via search injection [3].
Mitigation
The issue is fixed in Mongoose version 8.8.3 and later. Users should upgrade to the patched version immediately. For applications that cannot be updated immediately, avoid using user-supplied data directly in query conditions containing $where and implement strict input validation [4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mongoosenpm | >= 8.0.0-rc0, < 8.8.3 | 8.8.3 |
mongoosenpm | >= 7.0.0-rc0, < 7.8.3 | 7.8.3 |
mongoosenpm | >= 6.0.0-rc0, < 6.13.5 | 6.13.5 |
mongoosenpm | >= 3.6.0-rc0, < 5.13.23 | 5.13.23 |
Affected products
3- Mongoose/Mongoosedescription
- osv-coords2 versions
< 6.13.5+ 1 more
- (no CPE)range: < 6.13.5
- (no CPE)range: >= 8.0.0-rc0, < 8.8.3
Patches
3bbb6fa7ecb44Merge pull request #15105 from Automattic/vkarpov15/gh-15078
1 file changed · +9 −0
lib/helpers/populate/getModelsMapForPopulate.js+9 −0 modified@@ -207,6 +207,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'); + } if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) { match = Object.assign({}, match);
33679bcf8ca4fix: disallow using $where in match
3 files changed · +66 −5
lib/helpers/populate/assignVals.js+1 −5 modified@@ -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; -}
lib/helpers/populate/getModelsMapForPopulate.js+19 −0 modified@@ -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);
test/model.populate.test.js+46 −0 modified@@ -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,
c9e86bff7eeffix: disallow using $where in match
3 files changed · +66 −5
lib/helpers/populate/assignVals.js+1 −5 modified@@ -249,7 +249,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 = []; @@ -341,7 +341,3 @@ function isPopulatedObject(obj) { obj.$__ != null || leanPopulateMap.has(obj); } - -function noop(v) { - return v; -}
lib/helpers/populate/getModelsMapForPopulate.js+19 −0 modified@@ -184,6 +184,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; @@ -447,6 +456,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);
test/model.populate.test.js+46 −0 modified@@ -3641,6 +3641,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', async function() { const PersonSchema = new Schema({ name: String,
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- github.com/advisories/GHSA-m7xq-9374-9rvxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-53900ghsaADVISORY
- github.com/Automattic/mongoose/blob/master/CHANGELOG.mdghsaWEB
- github.com/Automattic/mongoose/commit/33679bcf8ca43d74e3e8ecd4cc224826772d805bghsaWEB
- github.com/Automattic/mongoose/commit/bbb6fa7ecb44bbaf5bea955d886378a1247bce0bghsaWEB
- github.com/Automattic/mongoose/commit/c9e86bff7eef477da75a29af62a06d41a835a156ghsaWEB
- github.com/Automattic/mongoose/compare/6.13.4...6.13.5ghsaWEB
- github.com/Automattic/mongoose/compare/7.8.2...7.8.3ghsaWEB
- github.com/Automattic/mongoose/compare/8.8.2...8.8.3ghsaWEB
- github.com/Automattic/mongoose/releasesghsaWEB
- github.com/github/advisory-database/pull/6769ghsaWEB
- github.com/github/advisory-database/pull/6776ghsaWEB
- www.npmjs.com/package/mongooseghsaWEB
News mentions
0No linked articles in our index yet.