CVE-2024-53900 (JavaScript)

Mongoose is vulnerable to search injection when user-controlled populate() match options can introduce a $where query operator into the MongoDB query used to load populated documents. An attacker who can influence populate match criteria, whether through literal match objects or match callback return values, can cause $where predicates to be propagated across query populate, document.populate(), and virtual populate paths. This risk applies to any $where occurrence in the resulting populate filter, including executable string or function predicates and $where keys nested inside logical containers or arrays produced when match callbacks are evaluated for multiple documents, not only to a top-level $where field. Because MongoDB $where represents server-side JavaScript/search predicates, allowing it to reach the database can execute attacker-controlled predicates or bypass intended query restrictions.

Weakness:
CWE-89: name: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.

Repository: https://github.com/Automattic/mongoose

Vulnerable code:
file_path: lib/helpers/populate/getModelsMapForPopulate.js
snippet: module.exports = function getModelsMapForPopulate(model, docs, options) {
  let doc;
  const len = docs.length;
  const map = [];
  const modelNameFromQuery = options.model && options.model.modelName || options.model;
  let schema;
  let refPath;
  let modelNames;
  const available = {};

  const modelSchema = model.schema;

  // Populating a nested path should always be a no-op re: #9073.
  // People shouldn't do this, but apparently they do.
  if (options._localModel != null && options._localModel.schema.nested[options.path]) {
    return [];
  }

  const _virtualRes = getVirtual(model.schema, options.path);
  const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  if (virtual != null) {
    return _virtualPopulate(model, docs, options, _virtualRes);
  }

  let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path);
  allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);

  const isStrictPopulateDisabled = options.strictPopulate === false || options.options?.strictPopulate === false;
  if (!isStrictPopulateDisabled && allSchemaTypes.length === 0 && options._localModel != null) {
    return new StrictPopulate(options._fullPath || options.path);
  }

  for (let i = 0; i < len; i++) {
    doc = docs[i];
    let justOne = null;

    const docSchema = doc != null && doc.$__ != null ? doc.$__schema : modelSchema;
    schema = getSchemaTypes(model, docSchema, doc, options.path);

    // Special case: populating a path that's a DocumentArray unless
    // there's an explicit `ref` or `refPath` re: gh-8946
    if (schema != null &&
        schema.$isMongooseDocumentArray &&
        schema.options.ref == null &&
        schema.options.refPath == null) {
      continue;
    }
    const isUnderneathDocArray = schema && schema.$parentSchemaDocArray;
    if (isUnderneathDocArray && get(options, 'options.sort') != null) {
      return new MongooseError('Cannot populate with `sort` on path ' + options.path +
        ' because it is a subproperty of a document array');
    }

    modelNames = null;
    let isRefPath = false;
    let normalizedRefPath = null;
    let schemaOptions = null;
    let modelNamesInOrder = null;

    if (schema != null && schema.instance === 'Embedded') {
      if (schema.options.ref) {
        const data = {
          localField: options.path + '._id',
          foreignField: '_id',
          justOne: true
        };
        const res = _getModelNames(doc, schema, modelNameFromQuery, model);

        const unpopulatedValue = mpath.get(options.path, doc);
        const id = mpath.get('_id', unpopulatedValue);
        addModelNamesToMap(model, map, available, res.modelNames, options, data, id, doc, schemaOptions, unpopulatedValue);
      }
      // No-op if no `ref` set. See gh-11538
      continue;
    }

    if (Array.isArray(schema)) {
      const schemasArray = schema;
      for (const _schema of schemasArray) {
        let _modelNames;
        let res;
        try {
          res = _getModelNames(doc, _schema, modelNameFromQuery, model);
          _modelNames = res.modelNames;
          isRefPath = isRefPath || res.isRefPath;
          normalizedRefPath = normalizedRefPath || res.refPath;
          justOne = res.justOne;
        } catch (error) {
          return error;
        }

        if (isRefPath && !res.isRefPath) {
          continue;
        }
        if (!_modelNames) {
          continue;
        }
        modelNames = modelNames || [];
        for (const modelName of _modelNames) {
          if (modelNames.indexOf(modelName) === -1) {
            modelNames.push(modelName);
          }
        }
      }
    } else {
      try {
        const res = _getModelNames(doc, schema, modelNameFromQuery, model);
        modelNames = res.modelNames;
        isRefPath = res.isRefPath;
        normalizedRefPath = normalizedRefPath || res.refPath;
        justOne = res.justOne;
        schemaOptions = get(schema, 'options.populate', null);
        // Dedupe, because `refPath` can return duplicates of the same model name,
        // and that causes perf issues.
        if (isRefPath) {
          modelNamesInOrder = modelNames;
          modelNames = Array.from(new Set(modelNames));
        }
      } catch (error) {
        return error;
      }

      if (!modelNames) {
        continue;
      }
    }

    const data = {};
    const localField = options.path;
    const foreignField = '_id';

    // `justOne = null` means we don't know from the schema whether the end
    // result should be an array or a single doc. This can result from
    // populating a POJO using `Model.populate()`
    if ('justOne' in options && options.justOne !== void 0) {
      justOne = options.justOne;
    } else if (schema && !schema[schemaMixedSymbol]) {
      // Skip Mixed types because we explicitly don't do casting on those.
      if (options.path.endsWith('.' + schema.path) || options.path === schema.path) {
        justOne = Array.isArray(schema) ?
          schema.every(schema => !schema.$isMongooseArray) :
          !schema.$isMongooseArray;
      }
    }

    if (!modelNames) {
      continue;
    }

    data.isVirtual = false;
    data.justOne = justOne;
    data.localField = localField;
    data.foreignField = foreignField;

    // Get local fields
    const ret = _getLocalFieldValues(doc, localField, model, options, null, schema);

    const id = String(utils.getValue(foreignField, doc));
    options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;

    let match = get(options, 'match', null);

    const hasMatchFunction = typeof match === 'function';
    if (hasMatchFunction) {
      match = match.call(doc, doc);
    }
    data.match = match;
    data.hasMatchFunction = hasMatchFunction;
    data.isRefPath = isRefPath;
    data.modelNamesInOrder = modelNamesInOrder;

    if (isRefPath) {
      const embeddedDiscriminatorModelNames = _findRefPathForDiscriminators(doc,
        modelSchema, data, options, normalizedRefPath, ret);

      modelNames = embeddedDiscriminatorModelNames || modelNames;
    }

    try {
      addModelNamesToMap(model, map, available, modelNames, options, data, ret, doc, schemaOptions);
    } catch (err) {
      return err;
    }
  }
  return map;

  function _getModelNames(doc, schema, modelNameFromQuery, model) {
    let modelNames;
    let isRefPath = false;
    let justOne = null;

    const originalSchema = schema;
    if (schema && schema.instance === 'Array') {
      schema = schema.caster;
    }
    if (schema && schema.$isSchemaMap) {
      schema = schema.$__schemaType;
    }

    const ref = schema && schema.options && schema.options.ref;
    refPath = schema && schema.options && schema.options.refPath;
    if (schema != null &&
        schema[schemaMixedSymbol] &&
        !ref &&
        !refPath &&
        !modelNameFromQuery) {
      return { modelNames: null };
    }

    if (modelNameFromQuery) {
      modelNames = [modelNameFromQuery]; // query options
    } else if (refPath != null) {
      if (typeof refPath === 'function') {
        const subdocPath = options.path.slice(0, options.path.length - schema.path.length - 1);
        const vals = mpath.get(subdocPath, doc, lookupLocalFields);
        const subdocsBeingPopulated = Array.isArray(vals) ?
          utils.array.flatten(vals) :
          (vals ? [vals] : []);

        modelNames = new Set();
        for (const subdoc of subdocsBeingPopulated) {
          refPath = refPath.call(subdoc, subdoc, options.path);
          modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
            forEach(name => modelNames.add(name));
        }
        modelNames = Array.from(modelNames);
      } else {
        modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
      }

      isRefPath = true;
    } else {
      let ref;
      let refPath;
      let schemaForCurrentDoc;
      let discriminatorValue;
      let modelForCurrentDoc = model;
      const discriminatorKey = model.schema.options.discriminatorKey;

      if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
        // `modelNameForFind` is the discriminator value, so we might need
        // find the discriminated model name
        const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
        if (discriminatorModel != null) {
          modelForCurrentDoc = discriminatorModel;
        } else {
          try {
            modelForCurrentDoc = _getModelFromConn(model.db, discriminatorValue);
          } catch (error) {
            return error;
          }
        }

        schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);

        if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
          schemaForCurrentDoc = schemaForCurrentDoc.caster;
        }
      } else {
        schemaForCurrentDoc = schema;
      }

      if (originalSchema && originalSchema.path.endsWith('.$*')) {
        justOne = !originalSchema.$isMongooseArray && !originalSchema._arrayPath;
      } else if (schemaForCurrentDoc != null) {
        justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath;
      }

      if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
        if (schemaForCurrentDoc != null &&
            typeof ref === 'function' &&
            options.path.endsWith('.' + schemaForCurrentDoc.path)) {
          // Ensure correct context for ref functions: subdoc, not top-level doc. See gh-8469
          modelNames = new Set();

          const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
          const vals = mpath.get(subdocPath, doc, lookupLocalFields);
          const subdocsBeingPopulated = Array.isArray(vals) ?
            utils.array.flatten(vals) :
            (vals ? [vals] : []);
          for (const subdoc of subdocsBeingPopulated) {
            modelNames.add(handleRefFunction(ref, subdoc));
          }

          if (subdocsBeingPopulated.length === 0) {
            modelNames = [handleRefFunction(ref, doc)];
          } else {
            modelNames = Array.from(modelNames);
          }
        } else {
          ref = handleRefFunction(ref, doc);
          modelNames = [ref];
        }
      } else if ((schemaForCurrentDoc = get(schema, 'options.refPath')) != null) {
        isRefPath = true;
        if (typeof refPath === 'function') {
          const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
          const vals = mpath.get(subdocPath, doc, lookupLocalFields);
          const subdocsBeingPopulated = Array.isArray(vals) ?
            utils.array.flatten(vals) :
            (vals ? [vals] : []);

          modelNames = new Set();
          for (const subdoc of subdocsBeingPopulated) {
            refPath = refPath.call(subdoc, subdoc, options.path);
            modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
              forEach(name => modelNames.add(name));
          }
          modelNames = Array.from(modelNames);
        } else {
          modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
        }
      }
    }

    if (!modelNames) {
      // `Model.populate()` on a POJO with no known local model. Default to using the `Model`
      if (options._localModel == null) {
        modelNames = [model.modelName];
      } else {
        return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
      }
    }

    if (!Array.isArray(modelNames)) {
      modelNames = [modelNames];
    }

    return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
  }
};

file_path: lib/helpers/populate/getModelsMapForPopulate.js
snippet: function _virtualPopulate(model, docs, options, _virtualRes) {
  const map = [];
  const available = {};
  const virtual = _virtualRes.virtual;

  for (const doc of docs) {
    let modelNames = null;
    const data = {};

    // localField and foreignField
    let localField;
    const virtualPrefix = _virtualRes.nestedSchemaPath ?
      _virtualRes.nestedSchemaPath + '.' : '';
    if (typeof options.localField === 'string') {
      localField = options.localField;
    } else if (typeof virtual.options.localField === 'function') {
      localField = virtualPrefix + virtual.options.localField.call(doc, doc);
    } else if (Array.isArray(virtual.options.localField)) {
      localField = virtual.options.localField.map(field => virtualPrefix + field);
    } else {
      localField = virtualPrefix + virtual.options.localField;
    }
    data.count = virtual.options.count;

    if (virtual.options.skip != null && !options.hasOwnProperty('skip')) {
      options.skip = virtual.options.skip;
    }
    if (virtual.options.limit != null && !options.hasOwnProperty('limit')) {
      options.limit = virtual.options.limit;
    }
    if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) {
      options.perDocumentLimit = virtual.options.perDocumentLimit;
    }
    let foreignField = virtual.options.foreignField;

    if (!localField || !foreignField) {
      return new MongooseError(`Cannot populate virtual \`${options.path}\` on model \`${model.modelName}\`, because options \`localField\` and / or \`foreignField\` are missing`);
    }

    if (typeof localField === 'function') {
      localField = localField.call(doc, doc);
    }
    if (typeof foreignField === 'function') {
      foreignField = foreignField.call(doc, doc);
    }

    data.isRefPath = false;

    // `justOne = null` means we don't know from the schema whether the end
    // result should be an array or a single doc. This can result from
    // populating a POJO using `Model.populate()`
    let justOne = null;
    if ('justOne' in options && options.justOne !== void 0) {
      justOne = options.justOne;
    }

    modelNames = virtual._getModelNamesForPopulate(doc);
    if (virtual.options.refPath) {
      justOne = !!virtual.options.justOne;
      data.isRefPath = true;
    } else if (virtual.options.ref) {
      justOne = !!virtual.options.justOne;
    }

    data.isVirtual = true;
    data.virtual = virtual;
    data.justOne = justOne;

    // `match`
    const baseMatch = get(data, 'virtual.options.match', null) ||
      get(data, 'virtual.options.options.match', null);
    let match = get(options, 'match', null) || baseMatch;

    let hasMatchFunction = typeof match === 'function';
    if (hasMatchFunction) {
      match = match.call(doc, doc, data.virtual);
    }

    if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) {
      match = Object.assign({}, match);
      for (let i = 1; i < localField.length; ++i) {
        match[foreignField[i]] = convertTo_id(mpath.get(localField[i], doc, lookupLocalFields), model.schema);
        hasMatchFunction = true;
      }

      localField = localField[0];
      foreignField = foreignField[0];
    }
    data.localField = localField;
    data.foreignField = foreignField;
    data.match = match;
    data.hasMatchFunction = hasMatchFunction;

    // Get local fields
    const ret = _getLocalFieldValues(doc, localField, model, options, virtual);

    try {
      addModelNamesToMap(model, map, available, modelNames, options, data, ret, doc);
    } catch (err) {
      return err;
    }
  }

  return map;
}

Fixed code:
file_path: lib/helpers/populate/getModelsMapForPopulate.js
snippet: module.exports = function getModelsMapForPopulate(model, docs, options) {
  let doc;
  const len = docs.length;
  const map = [];
  const modelNameFromQuery = options.model && options.model.modelName || options.model;
  let schema;
  let refPath;
  let modelNames;
  const available = {};

  const modelSchema = model.schema;

  // Populating a nested path should always be a no-op re: #9073.
  // People shouldn't do this, but apparently they do.
  if (options._localModel != null && options._localModel.schema.nested[options.path]) {
    return [];
  }

  const _virtualRes = getVirtual(model.schema, options.path);
  const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  if (virtual != null) {
    return _virtualPopulate(model, docs, options, _virtualRes);
  }

  let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path);
  allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);

  const isStrictPopulateDisabled = options.strictPopulate === false || options.options?.strictPopulate === false;
  if (!isStrictPopulateDisabled && allSchemaTypes.length === 0 && options._localModel != null) {
    return new StrictPopulate(options._fullPath || options.path);
  }

  for (let i = 0; i < len; i++) {
    doc = docs[i];
    let justOne = null;

    const docSchema = doc != null && doc.$__ != null ? doc.$__schema : modelSchema;
    schema = getSchemaTypes(model, docSchema, doc, options.path);

    // Special case: populating a path that's a DocumentArray unless
    // there's an explicit `ref` or `refPath` re: gh-8946
    if (schema != null &&
        schema.$isMongooseDocumentArray &&
        schema.options.ref == null &&
        schema.options.refPath == null) {
      continue;
    }
    const isUnderneathDocArray = schema && schema.$parentSchemaDocArray;
    if (isUnderneathDocArray && get(options, 'options.sort') != null) {
      return new MongooseError('Cannot populate with `sort` on path ' + options.path +
        ' because it is a subproperty of a document array');
    }

    modelNames = null;
    let isRefPath = false;
    let normalizedRefPath = null;
    let schemaOptions = null;
    let modelNamesInOrder = null;

    if (schema != null && schema.instance === 'Embedded') {
      if (schema.options.ref) {
        const data = {
          localField: options.path + '._id',
          foreignField: '_id',
          justOne: true
        };
        const res = _getModelNames(doc, schema, modelNameFromQuery, model);

        const unpopulatedValue = mpath.get(options.path, doc);
        const id = mpath.get('_id', unpopulatedValue);
        addModelNamesToMap(model, map, available, res.modelNames, options, data, id, doc, schemaOptions, unpopulatedValue);
      }
      // No-op if no `ref` set. See gh-11538
      continue;
    }

    if (Array.isArray(schema)) {
      const schemasArray = schema;
      for (const _schema of schemasArray) {
        let _modelNames;
        let res;
        try {
          res = _getModelNames(doc, _schema, modelNameFromQuery, model);
          _modelNames = res.modelNames;
          isRefPath = isRefPath || res.isRefPath;
          normalizedRefPath = normalizedRefPath || res.refPath;
          justOne = res.justOne;
        } catch (error) {
          return error;
        }

        if (isRefPath && !res.isRefPath) {
          continue;
        }
        if (!_modelNames) {
          continue;
        }
        modelNames = modelNames || [];
        for (const modelName of _modelNames) {
          if (modelNames.indexOf(modelName) === -1) {
            modelNames.push(modelName);
          }
        }
      }
    } else {
      try {
        const res = _getModelNames(doc, schema, modelNameFromQuery, model);
        modelNames = res.modelNames;
        isRefPath = res.isRefPath;
        normalizedRefPath = normalizedRefPath || res.refPath;
        justOne = res.justOne;
        schemaOptions = get(schema, 'options.populate', null);
        // Dedupe, because `refPath` can return duplicates of the same model name,
        // and that causes perf issues.
        if (isRefPath) {
          modelNamesInOrder = modelNames;
          modelNames = Array.from(new Set(modelNames));
        }
      } catch (error) {
        return error;
      }

      if (!modelNames) {
        continue;
      }
    }

    const data = {};
    const localField = options.path;
    const foreignField = '_id';

    // `justOne = null` means we don't know from the schema whether the end
    // result should be an array or a single doc. This can result from
    // populating a POJO using `Model.populate()`
    if ('justOne' in options && options.justOne !== void 0) {
      justOne = options.justOne;
    } else if (schema && !schema[schemaMixedSymbol]) {
      // Skip Mixed types because we explicitly don't do casting on those.
      if (options.path.endsWith('.' + schema.path) || options.path === schema.path) {
        justOne = Array.isArray(schema) ?
          schema.every(schema => !schema.$isMongooseArray) :
          !schema.$isMongooseArray;
      }
    }

    if (!modelNames) {
      continue;
    }

    data.isVirtual = false;
    data.justOne = justOne;
    data.localField = localField;
    data.foreignField = foreignField;

    // Get local fields
    const ret = _getLocalFieldValues(doc, localField, model, options, null, schema);

    const id = String(utils.getValue(foreignField, doc));
    options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;

    let match = get(options, 'match', null);

    const hasMatchFunction = typeof match === 'function';
    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;
    data.modelNamesInOrder = modelNamesInOrder;

    if (isRefPath) {
      const embeddedDiscriminatorModelNames = _findRefPathForDiscriminators(doc,
        modelSchema, data, options, normalizedRefPath, ret);

      modelNames = embeddedDiscriminatorModelNames || modelNames;
    }

    try {
      addModelNamesToMap(model, map, available, modelNames, options, data, ret, doc, schemaOptions);
    } catch (err) {
      return err;
    }
  }
  return map;

  function _getModelNames(doc, schema, modelNameFromQuery, model) {
    let modelNames;
    let isRefPath = false;
    let justOne = null;

    const originalSchema = schema;
    if (schema && schema.instance === 'Array') {
      schema = schema.caster;
    }
    if (schema && schema.$isSchemaMap) {
      schema = schema.$__schemaType;
    }

    const ref = schema && schema.options && schema.options.ref;
    refPath = schema && schema.options && schema.options.refPath;
    if (schema != null &&
        schema[schemaMixedSymbol] &&
        !ref &&
        !refPath &&
        !modelNameFromQuery) {
      return { modelNames: null };
    }

    if (modelNameFromQuery) {
      modelNames = [modelNameFromQuery]; // query options
    } else if (refPath != null) {
      if (typeof refPath === 'function') {
        const subdocPath = options.path.slice(0, options.path.length - schema.path.length - 1);
        const vals = mpath.get(subdocPath, doc, lookupLocalFields);
        const subdocsBeingPopulated = Array.isArray(vals) ?
          utils.array.flatten(vals) :
          (vals ? [vals] : []);

        modelNames = new Set();
        for (const subdoc of subdocsBeingPopulated) {
          refPath = refPath.call(subdoc, subdoc, options.path);
          modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
            forEach(name => modelNames.add(name));
        }
        modelNames = Array.from(modelNames);
      } else {
        modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
      }

      isRefPath = true;
    } else {
      let ref;
      let refPath;
      let schemaForCurrentDoc;
      let discriminatorValue;
      let modelForCurrentDoc = model;
      const discriminatorKey = model.schema.options.discriminatorKey;

      if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
        // `modelNameForFind` is the discriminator value, so we might need
        // find the discriminated model name
        const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
        if (discriminatorModel != null) {
          modelForCurrentDoc = discriminatorModel;
        } else {
          try {
            modelForCurrentDoc = _getModelFromConn(model.db, discriminatorValue);
          } catch (error) {
            return error;
          }
        }

        schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);

        if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
          schemaForCurrentDoc = schemaForCurrentDoc.caster;
        }
      } else {
        schemaForCurrentDoc = schema;
      }

      if (originalSchema && originalSchema.path.endsWith('.$*')) {
        justOne = !originalSchema.$isMongooseArray && !originalSchema._arrayPath;
      } else if (schemaForCurrentDoc != null) {
        justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath;
      }

      if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
        if (schemaForCurrentDoc != null &&
            typeof ref === 'function' &&
            options.path.endsWith('.' + schemaForCurrentDoc.path)) {
          // Ensure correct context for ref functions: subdoc, not top-level doc. See gh-8469
          modelNames = new Set();

          const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
          const vals = mpath.get(subdocPath, doc, lookupLocalFields);
          const subdocsBeingPopulated = Array.isArray(vals) ?
            utils.array.flatten(vals) :
            (vals ? [vals] : []);
          for (const subdoc of subdocsBeingPopulated) {
            modelNames.add(handleRefFunction(ref, subdoc));
          }

          if (subdocsBeingPopulated.length === 0) {
            modelNames = [handleRefFunction(ref, doc)];
          } else {
            modelNames = Array.from(modelNames);
          }
        } else {
          ref = handleRefFunction(ref, doc);
          modelNames = [ref];
        }
      } else if ((schemaForCurrentDoc = get(schema, 'options.refPath')) != null) {
        isRefPath = true;
        if (typeof refPath === 'function') {
          const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
          const vals = mpath.get(subdocPath, doc, lookupLocalFields);
          const subdocsBeingPopulated = Array.isArray(vals) ?
            utils.array.flatten(vals) :
            (vals ? [vals] : []);

          modelNames = new Set();
          for (const subdoc of subdocsBeingPopulated) {
            refPath = refPath.call(subdoc, subdoc, options.path);
            modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
              forEach(name => modelNames.add(name));
          }
          modelNames = Array.from(modelNames);
        } else {
          modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
        }
      }
    }

    if (!modelNames) {
      // `Model.populate()` on a POJO with no known local model. Default to using the `Model`
      if (options._localModel == null) {
        modelNames = [model.modelName];
      } else {
        return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
      }
    }

    if (!Array.isArray(modelNames)) {
      modelNames = [modelNames];
    }

    return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
  }
};

file_path: lib/helpers/populate/getModelsMapForPopulate.js
snippet:         for (const subdoc of subdocsBeingPopulated) {
          refPath = refPath.call(subdoc, subdoc, options.path);
          modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
            forEach(name => modelNames.add(name));
        }
        modelNames = Array.from(modelNames);
      } else {
        modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
      }

      isRefPath = true;
    } else {
      let ref;
      let refPath;
      let schemaForCurrentDoc;
      let discriminatorValue;
      let modelForCurrentDoc = model;
      const discriminatorKey = model.schema.options.discriminatorKey;

      if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
        // `modelNameForFind` is the discriminator value, so we might need
        // find the discriminated model name
        const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
        if (discriminatorModel != null) {
          modelForCurrentDoc = discriminatorModel;
        } else {
          try {
            modelForCurrentDoc = _getModelFromConn(model.db, discriminatorValue);
          } catch (error) {
            return error;
          }
        }

        schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);

        if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
          schemaForCurrentDoc = schemaForCurrentDoc.caster;
        }
      } else {
        schemaForCurrentDoc = schema;
      }

      if (originalSchema && originalSchema.path.endsWith('.$*')) {
        justOne = !originalSchema.$isMongooseArray && !originalSchema._arrayPath;
      } else if (schemaForCurrentDoc != null) {
        justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath;
      }

      if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
        if (schemaForCurrentDoc != null &&
            typeof ref === 'function' &&
            options.path.endsWith('.' + schemaForCurrentDoc.path)) {
          // Ensure correct context for ref functions: subdoc, not top-level doc. See gh-8469
          modelNames = new Set();

          const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
          const vals = mpath.get(subdocPath, doc, lookupLocalFields);
          const subdocsBeingPopulated = Array.isArray(vals) ?
            utils.array.flatten(vals) :