diff --git a/CHANGELOG.md b/CHANGELOG.md index ad815c10b9..5b29da9c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +6.13.6 / 2025-01-13 +=================== + * fix: disallow nested $where in populate match + 7.8.3 / 2024-11-26 ================== * fix: disallow using $where in match @@ -646,7 +650,7 @@ * fix(document): isModified should not be triggered when setting a nested boolean to the same value as previously #12994 [lpizzinidev](https://github.com/lpizzinidev) * fix(document): save newly set defaults underneath single nested subdocuments #13002 #12905 * fix(update): handle custom discriminator model name when casting update #12947 [wassil](https://github.com/wassil) - * fix(connection): handles unique autoincrement ID for connections #12990 [lpizzinidev](https://github.com/lpizzinidev) + * fix(connection): handles unique autoincrement ID for connections #12990 [lpizzinidev](https://github.com/lpizzinidev) * fix(types): fix type of options of Model.aggregate #12933 [ghost91-](https://github.com/ghost91-) * fix(types): fix "near" aggregation operator input type #12954 [Jokero](https://github.com/Jokero) * fix(types): add missing Top operator to AccumulatorOperator type declaration #12952 [lpizzinidev](https://github.com/lpizzinidev) @@ -675,7 +679,7 @@ * docs(typescript): add notes about virtual context to Mongoose 6 migration and TypeScript virtuals docs #12912 #12806 * docs(schematypes): removed dead link and fixed formatting #12897 #12885 [lpizzinidev](https://github.com/lpizzinidev) * docs: fix link to lean api #12910 [manniL](https://github.com/manniL) - * docs: list all possible strings for schema.pre in one place #12868 + * docs: list all possible strings for schema.pre in one place #12868 * docs: add list of known incompatible npm packages #12892 [IslandRhythms](https://github.com/IslandRhythms) 6.8.3 / 2023-01-06 diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 90d801951f..679e96424f 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -184,15 +184,7 @@ 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'); - } + throwOn$where(match); data.match = match; data.hasMatchFunction = hasMatchFunction; data.isRefPath = isRefPath; @@ -470,15 +462,7 @@ 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'); - } + throwOn$where(match); // Get local fields const ret = _getLocalFieldValues(doc, localField, model, options, virtual); @@ -751,3 +735,24 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz return modelNames; } + +/** + * Throw an error if there are any $where keys + */ + +function throwOn$where(match) { + if (match == null) { + return; + } + if (typeof match !== 'object') { + return; + } + for (const key of Object.keys(match)) { + if (key === '$where') { + throw new MongooseError('Cannot use $where filter with populate() match'); + } + if (match[key] != null && typeof match[key] === 'object') { + throwOn$where(match[key]); + } + } +} diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 9b49e8dc88..bf58c1db35 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -3582,21 +3582,34 @@ describe('model: populate:', function() { const parent = await Parent.create({ name: 'Anakin', child: child._id }); await assert.rejects( - () => Parent.findOne().populate({ path: 'child', match: { $where: 'console.log("oops!");' } }), + () => Parent.findOne().populate({ path: 'child', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }), /Cannot use \$where filter with populate\(\) match/ ); await assert.rejects( - () => Parent.find().populate({ path: 'child', match: { $where: 'console.log("oops!");' } }), + () => Parent.find().populate({ path: 'child', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }), /Cannot use \$where filter with populate\(\) match/ ); await assert.rejects( - () => parent.populate({ path: 'child', match: { $where: 'console.log("oops!");' } }), + () => parent.populate({ path: 'child', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }), /Cannot use \$where filter with populate\(\) match/ ); await assert.rejects( - () => Child.find().populate({ path: 'parent', match: { $where: 'console.log("oops!");' } }), + () => Child.find().populate({ path: 'parent', match: () => ({ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }) }), /Cannot use \$where filter with populate\(\) match/ ); + await assert.rejects( + () => Child.find().populate({ path: 'parent', match: () => ({ $or: [{ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }] }) }), + /Cannot use \$where filter with populate\(\) match/ + ); + await assert.rejects( + () => Child.find().populate({ path: 'parent', match: () => ({ $and: [{ $where: 'typeof console !== "undefined" ? doesNotExist("foo") : true;' }] }) }), + /Cannot use \$where filter with populate\(\) match/ + ); + + class MyClass {} + MyClass.prototype.$where = 'typeof console !== "undefined" ? doesNotExist("foo") : true;'; + // OK because sift only looks through own properties + await Child.find().populate({ path: 'parent', match: () => new MyClass() }); }); it('multiple source docs', async function() {