Security advisory GHSA-hxwc-5vw9-2w4w: NoSQL Injection in loopback-connector-mongodb
Package: loopback-connector-mongodb (npm), vulnerable version: 3.5.0, fixed in: 3.6.0
CWE: CWE-89
Files changed by the fix:
b/lib/mongodb.js

Fix diff:
diff -ruN --no-dereference /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.5.0/.npmignore /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.6.0/.npmignore
--- a/.npmignore
+++ b/.npmignore
@@ -1,6 +0,0 @@
-node_modules
-coverage
-.idea
-
-test
-.travis.yml
diff -ruN --no-dereference /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.5.0/CHANGES.md /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.6.0/CHANGES.md
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,4 +1,22 @@
-2018-07-24, Version 3.5.0
+2018-08-15, Version 3.6.0
+=========================
+
+ * docs: update with security consideration section (virkt25)
+
+ * fix: sanitize query by default (virkt25)
+
+ * change `count` to `countDocuments` (Rahmat Nugraha)
+
+ * add `useNewUrlParser` on validOptionNames (Rahmat Nugraha)
+
+ * Dedicated Model for testing disableDefaultSort (HugoPoi)
+
+ * Add disableDefaultSort in README (HugoPoi)
+
+ * Add settings disableDefaultSort for find method (HugoPoi)
+
+
+2018-07-23, Version 3.5.0
 =========================
 
  * chore: drop node 4 and update deps (Taranveer Virk)
diff -ruN --no-dereference /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.5.0/README.md /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.6.0/README.md
--- a/README.md
+++ b/README.md
@@ -64,6 +64,8 @@
   - Default is `false`.
   - If set to `true`, the database instance will not be attached to the datasource and the connection is deferred.
   - It will try to establish the connection automatically once users hit the endpoint. If the mongodb server is offline, the app will start, however, the endpoints will not work.
+- **disableDefaultSort**: Set to `true` to disable the default sorting
+  behavior on `id` column, this will help performance using indexed columns available in mongodb.
 
 ### Setting the url property in datasource.json
 
@@ -89,6 +91,23 @@
 
 For more information on setting data source configurations for different environments, see [Environment-specific configuration](https://loopback.io/doc/en/lb3/Environment-specific-configuration.html#data-source-configuration).
 
+## Security Considerations
+
+MongoDB Driver allows the `$where` operator to pass in JavaScript to execute on the Driver which can be used for NoSQL Injection. See [MongoDB: Server-side JavaScript](https://docs.mongodb.com/manual/core/server-side-javascript/) for more on this MongoDB feature.
+
+To protect users against this potential vulnerability, LoopBack will automatically **remove** the `$where` and `mapReduce` operators from a query before it's passed to the MongoDB Driver. If you need to use these properties from within LoopBack programatically, you can disable the sanitization by passing in an `options` object with `disableSanitization` property set to `true`.
+
+**Example:**
+```js
+Post.find(
+    {where: {$where: 'function() { /*JS function here*/}'}},
+    {disableSanitization: true},
+    (err, p) => {
+        // code to handle results / error.
+    }
+);
+```
+
 ## Type mappings
 
 See [LoopBack types](http://loopback.io/doc/en/lb3/LoopBack-types.html) for details on LoopBack's data types.
diff -ruN --no-dereference /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.5.0/lib/mongodb.js /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.6.0/lib/mongodb.js
--- a/lib/mongodb.js
+++ b/lib/mongodb.js
@@ -221,6 +221,7 @@
       'numberOfRetries',
       'auto_reconnect',
       'minSize',
+      'useNewUrlParser',
       // Ignored options
       'native_parser',
       // Legacy options
@@ -839,19 +840,22 @@
   return true;
 }
 
-MongoDB.prototype.buildWhere = function(model, where) {
+MongoDB.prototype.buildWhere = function(model, where, options) {
   var self = this;
   var query = {};
   if (where === null || typeof where !== 'object') {
     return query;
   }
+
+  where = sanitizeFilter(where, options);
+
   var idName = self.idName(model);
   Object.keys(where).forEach(function(k) {
     var cond = where[k];
     if (k === 'and' || k === 'or' || k === 'nor') {
       if (Array.isArray(cond)) {
         cond = cond.map(function(c) {
-          return self.buildWhere(model, c);
+          return self.buildWhere(model, c, options);
         });
       }
       query['$' + k] = cond;
@@ -936,16 +940,31 @@
   return query;
 };
 
-MongoDB.prototype.buildSort = function(model, order) {
+MongoDB.prototype.buildSort = function(model, order, options) {
   var sort = {};
   var idName = this.idName(model);
-  if (!order) {
+
+  var modelClass = this._models[model];
+
+  var disableDefaultSort = false;
+  if (this.settings.hasOwnProperty('disableDefaultSort')) {
+    disableDefaultSort = this.settings.disableDefaultSort;
+  }
+  if (modelClass.settings.hasOwnProperty('disableDefaultSort')) {
+    disableDefaultSort = modelClass.settings.disableDefaultSort;
+  }
+  if (options && options.hasOwnProperty('disableDefaultSort')) {
+    disableDefaultSort = options.disableDefaultSort;
+  }
+
+  if (!order && !disableDefaultSort) {
     var idNames = this.idNames(model);
     if (idNames && idNames.length) {
       order = idNames;
     }
   }
   if (order) {
+    order = sanitizeFilter(order, options);
     var keys = order;
     if (typeof keys === 'string') {
       keys = keys.split(',');
@@ -966,7 +985,7 @@
         sort[key] = 1;
       }
     }
-  } else {
+  } else if (!disableDefaultSort) {
     // order by _id by default
     sort = {_id: 1};
   }
@@ -1202,7 +1221,7 @@
   var idName = self.idName(model);
   var query = {};
   if (filter.where) {
-    query = self.buildWhere(model, filter.where);
+    query = self.buildWhere(model, filter.where, options);
   }
   var fields = filter.fields;
 
@@ -1224,7 +1243,7 @@
 
     // don't apply sorting if dealing with a geo query
     if (!hasNearFilter(filter.where)) {
-      var order = self.buildSort(model, filter.order);
+      var order = self.buildSort(model, filter.order, options);
       cursor.sort(order);
     }
 
@@ -1293,7 +1312,8 @@
     callback = where;
     where = undefined;
   }
-  where = self.buildWhere(model, where);
+  where = self.buildWhere(model, where, options);
+
   this.execute(model, 'remove', where || {}, function(err, info) {
     if (err) return callback && callback(err);
 
@@ -1320,8 +1340,8 @@
   if (self.debug) {
     debug('count', model, where);
   }
-  where = self.buildWhere(model, where);
-  this.execute(model, 'count', where, function(err, count) {
+  where = self.buildWhere(model, where, options);
+  this.execute(model, 'countDocuments', where, function(err, count) {
     if (self.debug) {
       debug('count.callback', model, err, count);
     }
@@ -1491,9 +1511,9 @@
   }
   var idName = this.idName(model);
 
-  where = self.buildWhere(model, where);
-  delete data[idName];
+  where = self.buildWhere(model, where, options);
 
+  delete data[idName];
   data = self.toDatabase(model, data);
 
   // Check for other operators and sanitize the data obj
@@ -1783,6 +1803,23 @@
   }
 };
 
+function sanitizeFilter(filter, options) {
+  options = Object.assign({}, options);
+  if (options && options.disableSanitization) return filter;
+  if (!filter || typeof filter !== 'object') return filter;
+
+  for (const key in filter) {
+    if (key === '$where' || key === 'mapReduce') {
+      debug(`sanitizeFilter: deleting ${key}`);
+      delete filter[key];
+    }
+  }
+
+  return filter;
+}
+
+exports.sanitizeFilter = sanitizeFilter;
+
 /**
  * Find a matching model instances by the filter or create a new instance
  *
@@ -1793,12 +1830,14 @@
  * @param {Object} filter The filter
  * @param {Function} [callback] The callback function
  */
-function optimizedFindOrCreate(model, filter, data, callback) {
+function optimizedFindOrCreate(model, filter, data, options, callback) {
   var self = this;
   if (self.debug) {
     debug('findOrCreate', model, filter, data);
   }
 
+  if (!callback) callback = options;
+
   var idValue = self.getIdValue(model, data);
   var idName = self.idName(model);
 
@@ -1821,10 +1860,10 @@
       id = self.coerceId(model, id);
       filter.where._id = id;
     }
-    query = self.buildWhere(model, filter.where);
+    query = self.buildWhere(model, filter.where, options);
   }
 
-  var sort = self.buildSort(model, filter.order);
+  var sort = self.buildSort(model, filter.order, options);
 
   this.collection(model).findOneAndUpdate(
     query,
diff -ruN --no-dereference /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.5.0/package.json /home/ubuntu/closed-source-intelligence/data/cache/npm-tarballs/loopback-connector-mongodb-3.6.0/package.json
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "loopback-connector-mongodb",
-  "version": "3.5.0",
+  "version": "3.6.0",
   "description": "The official MongoDB connector for the LoopBack framework.",
   "engines": {
     "node": ">=6"