CVE-2022-31112 (JavaScript, published 2022-06-30T17:15Z)
Description: value: 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 v3 base score: 8.2)
Weakness: CWE-212 \u2014 Improper Removal of Sensitive Information Before Storage or Transfer
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.
Repository: https://github.com/parse-community/parse-server
Files changed:
spec/ParseLiveQuery.spec.js
src/Controllers/DatabaseController.js
src/LiveQuery/ParseCloudCodePublisher.js
src/LiveQuery/ParseLiveQueryServer.js
Commit message: 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)



Vulnerable code:
  schema: SchemaController.SchemaController,
  const perms = schema.getClassLevelPermissions(className);
    schema: SchemaController.SchemaController,
    const perms = schema.getClassLevelPermissions(className);
import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers';
import { getCacheController } from '../Controllers';
            if (
              (deletedParseObject.className === '_User' ||
                deletedParseObject.className === '_Session') &&
              !client.hasMasterKey
            ) {
              delete deletedParseObject.sessionToken;
              delete deletedParseObject.authData;
            }
            if (
              (currentParseObject.className === '_User' ||
                currentParseObject.className === '_Session') &&
              !client.hasMasterKey
            ) {
              delete currentParseObject.sessionToken;
              delete originalParseObject?.sessionToken;
              delete currentParseObject.authData;
              delete originalParseObject?.authData;
            }

Fixed code:
  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' }),
    ]);
  });

  schema: SchemaController.SchemaController | any,
  const perms =
    schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
    schema: SchemaController.SchemaController | any,
    const perms =
      schema && schema.getClassLevelPermissions
        ? schema.getClassLevelPermissions(className)
        : schema;
  static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
module.exports.filterSensitiveData = filterSensitiveData;
    if (request.classLevelPermissions) {
      message.classLevelPermissions = request.classLevelPermissions;
    }
import {
  runLiveQueryEventHandlers,
  getTrigger,
  runTrigger,
  resolveError,
  toJSONwithObjects,
} from '../triggers';
import { getCacheController, getDatabaseController } from '../Controllers';
import DatabaseController from '../Controllers/DatabaseController';
            await this._filterSensitiveData(
              classLevelPermissions,
              res,
              client,
              requestId,
              op,
              subscription.query
            );
            await this._filterSensitiveData(
              classLevelPermissions,
              res,
              client,
              requestId,
              op,
              subscription.query
            );
  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);
  }