CVE-2023-3696: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: Prototype Pollution in GitHub repository automattic/mongoose prior to 7.3.4.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 9.8
Published: 2023-07-17T01:15Z
Repository: https://github.com/automattic/mongoose
Language: JavaScript
Files changed: lib/document.js, test/document.test.js
Commit date: 2023-07-11 10:02:28 -0400
Commit message: fix: avoid prototype pollution on init

Diff: diff --git a/lib/document.js b/lib/document.js
index 831ed0885..d19094891 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -738,10 +738,14 @@ function init(self, obj, doc, opts, prefix) {
     _init(index++);
   }
 
   function _init(index) {
     i = keys[index];
+    // avoid prototype pollution
+    if (i === '__proto__' || i === 'constructor') {
+      return;
+    }
     path = prefix + i;
     schemaType = docSchema.path(path);
 
     // Should still work if not a model-level discriminator, but should not be
     // necessary. This is *only* to catch the case where we queried using the
diff --git a/test/document.test.js b/test/document.test.js
index 7dbc9cd79..5eb33c0f8 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -12276,10 +12276,28 @@ describe('document', function() {
 
     const fromDb = await Test.findById(test);
     assert.equal(fromDb.obj.subArr.length, 1);
     assert.equal(fromDb.obj.subArr[0].str, 'subArr.test1');
   });
+
+  it('avoids prototype pollution on init', async function() {
+    const Example = db.model('Example', new Schema({ hello: String }));
+
+    const example = await new Example({ hello: 'world!' }).save();
+    await Example.findByIdAndUpdate(example._id, {
+      $rename: {
+        hello: '__proto__.polluted'
+      }
+    });
+
+    // this is what causes the pollution
+    await Example.find();
+
+    const test = {};
+    assert.strictEqual(test.polluted, undefined);
+    assert.strictEqual(Object.prototype.polluted, undefined);
+  });
 });
 
 describe('Check if instance function that is supplied in schema option is availabe', function() {
   it('should give an instance function back rather than undefined', function ModelJS() {
     const testSchema = new mongoose.Schema({}, { methods: { instanceFn() { return 'Returned from DocumentInstanceFn'; } } });

Fixed code:     // avoid prototype pollution
    if (i === '__proto__' || i === 'constructor') {
      return;
    }

  it('avoids prototype pollution on init', async function() {
    const Example = db.model('Example', new Schema({ hello: String }));

    const example = await new Example({ hello: 'world!' }).save();
    await Example.findByIdAndUpdate(example._id, {
      $rename: {
        hello: '__proto__.polluted'
      }
    });

    // this is what causes the pollution
    await Example.find();

    const test = {};
    assert.strictEqual(test.polluted, undefined);
    assert.strictEqual(Object.prototype.polluted, undefined);
  });