CVE-2022-31112 (published 2022-06-30T17:15Z)

Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. In affected versions parse Server LiveQuery does not remove protected fields in classes, passing them to the client. The LiveQueryController now removes protected fields from the client response. Users are advised to upgrade. Users unable t upgrade should use `Parse.Cloud.afterLiveQueryEvent` to manually remove protected fields.

Severity: MEDIUM
CVSS v2 base score: 6.4 (AV:N/AC:L/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 8.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N)

Weakness information:
cwe_id: CWE-212
cwe_name: Improper Removal of Sensitive Information Before Storage or Transfer
description: The product stores, transfers, or shares a resource that contains sensitive information, but it does not properly remove that information before the product makes the resource available to unauthorized actors.
extended_description: 
            Resources that may contain sensitive data include documents, packets, messages, databases, etc. While this data may be useful to an individual user or small set of users who share the resource, it may need to be removed before the resource can be shared outside of the trusted group. The process of removal is sometimes called cleansing or scrubbing.
            For example, a product for editing documents might not remove sensitive data such as reviewer comments or the local pathname where the document is stored. Or, a proxy might not remove an internal IP address from headers before making an outgoing request to an Internet site.
         
         

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: 2022-06-30 12:26:39+02:00
msg: fix: protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) (#8073)
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: ParseLiveQuery.spec.js
change_type: ModificationType.MODIFY
diff: @@ -1066,6 +1066,52 @@ describe('ParseLiveQuery', function () {
     }
   });
 
+  it('should strip out protected fields', async () => {
+    await reconfigureServer({
+      liveQuery: { classNames: ['Test'] },
+      startLiveQueryServer: true,
+    });
+    const obj1 = new Parse.Object('Test');
+    obj1.set('foo', 'foo');
+    obj1.set('bar', 'bar');
+    obj1.set('qux', 'qux');
+    await obj1.save();
+    const config = Config.get(Parse.applicationId);
+    const schemaController = await config.database.loadSchema();
+    await schemaController.updateClass(
+      'Test',
+      {},
+      {
+        get: { '*': true },
+        find: { '*': true },
+        update: { '*': true },
+        protectedFields: {
+          '*': ['foo'],
+        },
+      }
+    );
+    const object = await obj1.fetch();
+    expect(object.get('foo')).toBe(undefined);
+    expect(object.get('bar')).toBeDefined();
+    expect(object.get('qux')).toBeDefined();
+
+    const subscription = await new Parse.Query('Test').subscribe();
+    await Promise.all([
+      new Promise(resolve => {
+        subscription.on('update', (obj, original) => {
+          expect(obj.get('foo')).toBe(undefined);
+          expect(obj.get('bar')).toBeDefined();
+          expect(obj.get('qux')).toBeDefined();
+          expect(original.get('foo')).toBe(undefined);
+          expect(original.get('bar')).toBeDefined();
+          expect(original.get('qux')).toBeDefined();
+          resolve();
+        });
+      }),
+      obj1.save({ foo: 'abc' }),
+    ]);
+  });
+
   afterEach(async function (done) {
     const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
     client.close();

programming_language: TypeScript
filename: DatabaseController.js
change_type: ModificationType.MODIFY
diff: @@ -127,7 +127,7 @@ const filterSensitiveData = (
   aclGroup: any[],
   auth: any,
   operation: any,
-  schema: SchemaController.SchemaController,
+  schema: SchemaController.SchemaController | any,
   className: string,
   protectedFields: null | Array<any>,
   object: any
@@ -136,7 +136,8 @@ const filterSensitiveData = (
   if (auth && auth.user) userId = auth.user.id;
 
   // replace protectedFields when using pointer-permissions
-  const perms = schema.getClassLevelPermissions(className);
+  const perms =
+    schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
   if (perms) {
     const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
 
@@ -1533,14 +1534,17 @@ class DatabaseController {
   }
 
   addProtectedFields(
-    schema: SchemaController.SchemaController,
+    schema: SchemaController.SchemaController | any,
     className: string,
     query: any = {},
     aclGroup: any[] = [],
     auth: any = {},
     queryOptions: FullQueryOptions = {}
   ): null | string[] {
-    const perms = schema.getClassLevelPermissions(className);
+    const perms =
+      schema && schema.getClassLevelPermissions
+        ? schema.getClassLevelPermissions(className)
+        : schema;
     if (!perms) return null;
 
     const protectedFields = perms.protectedFields;
@@ -1806,8 +1810,10 @@ class DatabaseController {
   }
 
   static _validateQuery: any => void;
+  static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
 }
 
 module.exports = DatabaseController;
 // Expose validateQuery for tests
 module.exports._validateQuery = validateQuery;
+module.exports.filterSensitiveData = filterSensitiveData;

programming_language: TypeScript
filename: ParseCloudCodePublisher.js
change_type: ModificationType.MODIFY
diff: @@ -33,6 +33,9 @@ class ParseCloudCodePublisher {
     if (request.original) {
       message.originalParseObject = request.original._toFullJSON();
     }
+    if (request.classLevelPermissions) {
+      message.classLevelPermissions = request.classLevelPermissions;
+    }
     this.parsePublisher.publish(type, JSON.stringify(message));
   }
 }

programming_language: TypeScript
filename: ParseLiveQueryServer.js
change_type: ModificationType.MODIFY
diff: @@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
 import SchemaController from '../Controllers/SchemaController';
 import _ from 'lodash';
 import { v4 as uuidv4 } from 'uuid';
-import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers';
+import {
+  runLiveQueryEventHandlers,
+  getTrigger,
+  runTrigger,
+  resolveError,
+  toJSONwithObjects,
+} from '../triggers';
 import { getAuthForSessionToken, Auth } from '../Auth';
-import { getCacheController } from '../Controllers';
+import { getCacheController, getDatabaseController } from '../Controllers';
 import LRU from 'lru-cache';
 import UserRouter from '../Routers/UsersRouter';
+import DatabaseController from '../Controllers/DatabaseController';
 
 class ParseLiveQueryServer {
   clients: Map;
@@ -185,14 +192,14 @@ class ParseLiveQueryServer {
             if (res.object && typeof res.object.toJSON === 'function') {
               deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
             }
-            if (
-              (deletedParseObject.className === '_User' ||
-                deletedParseObject.className === '_Session') &&
-              !client.hasMasterKey
-            ) {
-              delete deletedParseObject.sessionToken;
-              delete deletedParseObject.authData;
-            }
+            await this._filterSensitiveData(
+              classLevelPermissions,
+              res,
+              client,
+              requestId,
+              op,
+              subscription.query
+            );
             client.pushDelete(requestId, deletedParseObject);
           } catch (e) {
             const error = resolveError(e);
@@ -339,16 +346,14 @@ class ParseLiveQueryServer {
                 res.original.className || className
               );
             }
-            if (
-              (currentParseObject.className === '_User' ||
-                currentParseObject.className === '_Session') &&
-              !client.hasMasterKey
-            ) {
-              delete currentParseObject.sessionToken;
-              delete originalParseObject?.sessionToken;
-              delete currentParseObject.authData;
-              delete originalParseObject?.authData;
-            }
+            await this._filterSensitiveData(
+              classLevelPermissions,
+              res,
+              client,
+              requestId,
+              op,
+              subscription.query
+            );
             const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
             if (client[functionName]) {
               client[functionName](requestId, currentParseObject, originalParseObject);
@@ -540,6 +545,54 @@ class ParseLiveQueryServer {
     // return rolesQuery.find({useMasterKey:true});
   }
 
+  async _filterSensitiveData(
+    classLevelPermissions: ?any,
+    res: any,
+    client: any,
+    requestId: number,
+    op: string,
+    query: any
+  ) {
+    const subscriptionInfo = client.getSubscriptionInfo(requestId);
+    const aclGroup = ['*'];
+    let clientAuth;
+    if (typeof subscriptionInfo !== 'undefined') {
+      const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
+      if (userId) {
+        aclGroup.push(userId);
+      }
+      clientAuth = auth;
+    }
+    const filter = obj => {
+      if (!obj) {
+        return;
+      }
+      let protectedFields = classLevelPermissions?.protectedFields || [];
+      if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
+        protectedFields = getDatabaseController(this.config).addProtectedFields(
+          classLevelPermissions,
+          res.object.className,
+          query,
+          aclGroup,
+          clientAuth
+        );
+      }
+      return DatabaseController.filterSensitiveData(
+        client.hasMasterKey,
+        aclGroup,
+        clientAuth,
+        op,
+        classLevelPermissions,
+        res.object.className,
+        protectedFields,
+        obj,
+        query
+      );
+    };
+    res.object = filter(res.object);
+    res.original = filter(res.original);
+  }
+
   _getCLPOperation(query: any) {
     return typeof query === 'object' &&
       Object.keys(query).length == 1 &&

programming_language: TypeScript
repo_url: https://github.com/parse-community/parse-server
commit_details: repo_url: https://github.com/parse-community/parse-server
author: Manuel
author_date: 2022-06-30 13:01:40+02:00
msg: fix: protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] (#8076)
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: ParseLiveQuery.spec.js
change_type: ModificationType.MODIFY
diff: @@ -1110,6 +1110,52 @@ describe('ParseLiveQuery', function () {
     }
   });
 
+  it('should strip out protected fields', async () => {
+    await reconfigureServer({
+      liveQuery: { classNames: ['Test'] },
+      startLiveQueryServer: true,
+    });
+    const obj1 = new Parse.Object('Test');
+    obj1.set('foo', 'foo');
+    obj1.set('bar', 'bar');
+    obj1.set('qux', 'qux');
+    await obj1.save();
+    const config = Config.get(Parse.applicationId);
+    const schemaController = await config.database.loadSchema();
+    await schemaController.updateClass(
+      'Test',
+      {},
+      {
+        get: { '*': true },
+        find: { '*': true },
+        update: { '*': true },
+        protectedFields: {
+          '*': ['foo'],
+        },
+      }
+    );
+    const object = await obj1.fetch();
+    expect(object.get('foo')).toBe(undefined);
+    expect(object.get('bar')).toBeDefined();
+    expect(object.get('qux')).toBeDefined();
+
+    const subscription = await new Parse.Query('Test').subscribe();
+    await Promise.all([
+      new Promise(resolve => {
+        subscription.on('update', (obj, original) => {
+          expect(obj.get('foo')).toBe(undefined);
+          expect(obj.get('bar')).toBeDefined();
+          expect(obj.get('qux')).toBeDefined();
+          expect(original.get('foo')).toBe(undefined);
+          expect(original.get('bar')).toBeDefined();
+          expect(original.get('qux')).toBeDefined();
+          resolve();
+        });
+      }),
+      obj1.save({ foo: 'abc' }),
+    ]);
+  });
+
   afterEach(async function (done) {
     const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
     client.close();

programming_language: TypeScript
filename: DatabaseController.js
change_type: ModificationType.MODIFY
diff: @@ -127,7 +127,7 @@ const filterSensitiveData = (
   aclGroup: any[],
   auth: any,
   operation: any,
-  schema: SchemaController.SchemaController,
+  schema: SchemaController.SchemaController | any,
   className: string,
   protectedFields: null | Array<any>,
   object: any
@@ -136,7 +136,8 @@ const filterSensitiveData = (
   if (auth && auth.user) userId = auth.user.id;
 
   // replace protectedFields when using pointer-permissions
-  const perms = schema.getClassLevelPermissions(className);
+  const perms =
+    schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
   if (perms) {
     const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
 
@@ -1533,14 +1534,17 @@ class DatabaseController {
   }
 
   addProtectedFields(
-    schema: SchemaController.SchemaController,
+    schema: SchemaController.SchemaController | any,
     className: string,
     query: any = {},
     aclGroup: any[] = [],
     auth: any = {},
     queryOptions: FullQueryOptions = {}
   ): null | string[] {
-    const perms = schema.getClassLevelPermissions(className);
+    const perms =
+      schema && schema.getClassLevelPermissions
+        ? schema.getClassLevelPermissions(className)
+        : schema;
     if (!perms) return null;
 
     const protectedFields = perms.protectedFields;
@@ -1806,8 +1810,10 @@ class DatabaseController {
   }
 
   static _validateQuery: any => void;
+  static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
 }
 
 module.exports = DatabaseController;
 // Expose validateQuery for tests
 module.exports._validateQuery = validateQuery;
+module.exports.filterSensitiveData = filterSensitiveData;

programming_language: TypeScript
filename: ParseCloudCodePublisher.js
change_type: ModificationType.MODIFY
diff: @@ -40,6 +40,9 @@ class ParseCloudCodePublisher {
     if (request.original) {
       message.originalParseObject = request.original._toFullJSON();
     }
+    if (request.classLevelPermissions) {
+      message.classLevelPermissions = request.classLevelPermissions;
+    }
     this.parsePublisher.publish(type, JSON.stringify(message));
   }
 }

programming_language: TypeScript
filename: ParseLiveQueryServer.js
change_type: ModificationType.MODIFY
diff: @@ -18,9 +18,10 @@ import {
   toJSONwithObjects,
 } from '../triggers';
 import { getAuthForSessionToken, Auth } from '../Auth';
-import { getCacheController } from '../Controllers';
+import { getCacheController, getDatabaseController } from '../Controllers';
 import LRU from 'lru-cache';
 import UserRouter from '../Routers/UsersRouter';
+import DatabaseController from '../Controllers/DatabaseController';
 
 class ParseLiveQueryServer {
   clients: Map;
@@ -196,14 +197,14 @@ class ParseLiveQueryServer {
             if (res.object && typeof res.object.toJSON === 'function') {
               deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
             }
-            if (
-              (deletedParseObject.className === '_User' ||
-                deletedParseObject.className === '_Session') &&
-              !client.hasMasterKey
-            ) {
-              delete deletedParseObject.sessionToken;
-              delete deletedParseObject.authData;
-            }
+            await this._filterSensitiveData(
+              classLevelPermissions,
+              res,
+              client,
+              requestId,
+              op,
+              subscription.query
+            );
             client.pushDelete(requestId, deletedParseObject);
           } catch (e) {
             const error = resolveError(e);
@@ -350,16 +351,14 @@ class ParseLiveQueryServer {
                 res.original.className || className
               );
             }
-            if (
-              (currentParseObject.className === '_User' ||
-                currentParseObject.className === '_Session') &&
-              !client.hasMasterKey
-            ) {
-              delete currentParseObject.sessionToken;
-              delete originalParseObject?.sessionToken;
-              delete currentParseObject.authData;
-              delete originalParseObject?.authData;
-            }
+            await this._filterSensitiveData(
+              classLevelPermissions,
+              res,
+              client,
+              requestId,
+              op,
+              subscription.query
+            );
             const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
             if (client[functionName]) {
               client[functionName](requestId, currentParseObject, originalParseObject);
@@ -577,6 +576,54 @@ class ParseLiveQueryServer {
     // return rolesQuery.find({useMasterKey:true});
   }
 
+  async _filterSensitiveData(
+    classLevelPermissions: ?any,
+    res: any,
+    client: any,
+    requestId: number,
+    op: string,
+    query: any
+  ) {
+    const subscriptionInfo = client.getSubscriptionInfo(requestId);
+    const aclGroup = ['*'];
+    let clientAuth;
+    if (typeof subscriptionInfo !== 'undefined') {
+      const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
+      if (userId) {
+        aclGroup.push(userId);
+      }
+      clientAuth = auth;
+    }
+    const filter = obj => {
+      if (!obj) {
+        return;
+      }
+      let protectedFields = classLevelPermissions?.protectedFields || [];
+      if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
+        protectedFields = getDatabaseController(this.config).addProtectedFields(
+          classLevelPermissions,
+          res.object.className,
+          query,
+          aclGroup,
+          clientAuth
+        );
+      }
+      return DatabaseController.filterSensitiveData(
+        client.hasMasterKey,
+        aclGroup,
+        clientAuth,
+        op,
+        classLevelPermissions,
+        res.object.className,
+        protectedFields,
+        obj,
+        query
+      );
+    };
+    res.object = filter(res.object);
+    res.original = filter(res.original);
+  }
+
   _getCLPOperation(query: any) {
     return typeof query === 'object' &&
       Object.keys(query).length == 1 &&

programming_language: TypeScript