CVE-2023-36475 (published 2023-06-28T23:15Z)

Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 5.5.2 and 6.2.1, an attacker can use a prototype pollution sink to trigger a remote code execution through the MongoDB BSON parser. A patch is available in versions 5.5.2 and 6.2.1.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-1321
cwe_name: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
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.
extended_description: 
				By adding or modifying attributes of an object prototype, it is possible to create attributes that exist on every object, or replace critical attributes with malicious ones. This can be problematic if the product depends on existence or non-existence of certain attributes, or uses pre-defined attributes of object prototype (such as hasOwnProperty, toString or valueOf).
				This weakness is usually exploited by using a special attribute of objects called proto,  constructor or prototype. Such attributes give access to the object prototype. This weakness is often found in code that assigns object attributes based on user input, or merges or clones objects recursively.
			
			

Fix information:
repo_url: https://github.com/parse-community/parse-server
commit_details: repo_url: https://github.com/parse-community/parse-server
author: Manuel
author_date: 2023-06-28 22:57:25+02:00
msg: fix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) (#8674)
repository: repo_url: https://github.com/parse-community/parse-server
repo_name: parse-community/parse-server
description: Parse Server for Node.js / Express
repo_language: JavaScript
owner: parse-community
file_changes: filename: .eslintrc.json
change_type: ModificationType.MODIFY
diff: @@ -25,5 +25,8 @@
         "space-infix-ops": "error",
         "no-useless-escape": "off",
         "require-atomic-updates": "off"
+    },
+    "globals": {
+        "Parse": true
     }
 }

programming_language: JavaScript
filename: vulnerabilities.spec.js
change_type: ModificationType.MODIFY
diff: @@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
       );
     });
 
+    it('denies creating global config with polluted data', async () => {
+      const headers = {
+        'Content-Type': 'application/json',
+        'X-Parse-Application-Id': 'test',
+        'X-Parse-Master-Key': 'test',
+      };
+      const params = {
+        method: 'PUT',
+        url: 'http://localhost:8378/1/config',
+        json: true,
+        body: {
+          params: {
+            welcomeMesssage: 'Welcome to Parse',
+            foo: { _bsontype: 'Code', code: 'shell' },
+          },
+        },
+        headers,
+      };
+      const response = await request(params).catch(e => e);
+      expect(response.status).toBe(400);
+      const text = JSON.parse(response.text);
+      expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
+      expect(text.error).toBe(
+        'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+      );
+    });
+
+    it('denies direct database write wih prohibited keys', async () => {
+      const Config = require('../lib/Config');
+      const config = Config.get(Parse.applicationId);
+      const user = {
+        objectId: '1234567890',
+        username: 'hello',
+        password: 'pass',
+        _session_token: 'abc',
+        foo: { _bsontype: 'Code', code: 'shell' },
+      };
+      await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
+        new Parse.Error(
+          Parse.Error.INVALID_KEY_NAME,
+          'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+        )
+      );
+    });
+
+    it('denies direct database update wih prohibited keys', async () => {
+      const Config = require('../lib/Config');
+      const config = Config.get(Parse.applicationId);
+      const user = {
+        objectId: '1234567890',
+        username: 'hello',
+        password: 'pass',
+        _session_token: 'abc',
+        foo: { _bsontype: 'Code', code: 'shell' },
+      };
+      await expectAsync(
+        config.database.update('_User', { _id: user.objectId }, user)
+      ).toBeRejectedWith(
+        new Parse.Error(
+          Parse.Error.INVALID_KEY_NAME,
+          'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
+        )
+      );
+    });
+
     it('denies creating a hook with polluted data', async () => {
       const express = require('express');
       const bodyParser = require('body-parser');

programming_language: TypeScript
filename: DatabaseController.js
change_type: ModificationType.MODIFY
diff: @@ -475,6 +475,11 @@ class DatabaseController {
     validateOnly: boolean = false,
     validSchemaController: SchemaController.SchemaController
   ): Promise<any> {
+    try {
+      Utils.checkProhibitedKeywords(this.options, update);
+    } catch (error) {
+      return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+    }
     const originalQuery = query;
     const originalUpdate = update;
     // Make a copy of the object, so we don't mutate the incoming data.
@@ -805,6 +810,11 @@ class DatabaseController {
     validateOnly: boolean = false,
     validSchemaController: SchemaController.SchemaController
   ): Promise<any> {
+    try {
+      Utils.checkProhibitedKeywords(this.options, object);
+    } catch (error) {
+      return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+    }
     // Make a copy of the object, so we don't mutate the incoming data.
     const originalObject = object;
     object = transformObjectACL(object);

programming_language: TypeScript
filename: RestWrite.js
change_type: ModificationType.MODIFY
diff: @@ -64,8 +64,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
     }
   }
 
-  this.checkProhibitedKeywords(data);
-
   // When the operation is complete, this.response may have several
   // fields.
   // response: the actual data to be returned
@@ -298,7 +296,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
           delete this.data.objectId;
         }
       }
-      this.checkProhibitedKeywords(this.data);
+      try {
+        Utils.checkProhibitedKeywords(this.config, this.data);
+      } catch (error) {
+        throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error);
+      }
     });
 };
 
@@ -1756,20 +1758,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
   return response;
 };
 
-RestWrite.prototype.checkProhibitedKeywords = function (data) {
-  if (this.config.requestKeywordDenylist) {
-    // Scan request data for denied keywords
-    for (const keyword of this.config.requestKeywordDenylist) {
-      const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
-      if (match) {
-        throw new Parse.Error(
-          Parse.Error.INVALID_KEY_NAME,
-          `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
-        );
-      }
-    }
-  }
-};
-
 export default RestWrite;
 module.exports = RestWrite;

programming_language: JavaScript
filename: FilesRouter.js
change_type: ModificationType.MODIFY
diff: @@ -175,22 +175,12 @@ export class FilesRouter {
     const base64 = req.body.toString('base64');
     const file = new Parse.File(filename, { base64 }, contentType);
     const { metadata = {}, tags = {} } = req.fileData || {};
-    if (req.config && req.config.requestKeywordDenylist) {
-      // Scan request data for denied keywords
-      for (const keyword of req.config.requestKeywordDenylist) {
-        const match =
-          Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
-          Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
-        if (match) {
-          next(
-            new Parse.Error(
-              Parse.Error.INVALID_KEY_NAME,
-              `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
-            )
-          );
-          return;
-        }
-      }
+    try {
+      Utils.checkProhibitedKeywords(config, metadata);
+      Utils.checkProhibitedKeywords(config, tags);
+    } catch (error) {
+      next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
+      return;
     }
     file.setTags(tags);
     file.setMetadata(metadata);

programming_language: TypeScript
filename: Utils.js
change_type: ModificationType.MODIFY
diff: @@ -358,6 +358,18 @@ class Utils {
     }
     return false;
   }
+
+  static checkProhibitedKeywords(config, data) {
+    if (config?.requestKeywordDenylist) {
+      // Scan request data for denied keywords
+      for (const keyword of config.requestKeywordDenylist) {
+        const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
+        if (match) {
+          throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`;
+        }
+      }
+    }
+  }
 }
 
 module.exports = Utils;

programming_language: TypeScript