CVE-2023-41058 (JavaScript, published 2023-09-04T23:15Z)
Description: value: Parse Server is an open source backend server. In affected versions the Parse Cloud trigger `beforeFind` is not invoked in certain conditions of `Parse.Query`. This can pose a vulnerability for deployments where the `beforeFind` trigger is used as a security layer to modify the incoming query. The vulnerability has been fixed by refactoring the internal query pipeline for a more concise code structure and implementing a patch to ensure the `beforeFind` trigger is invoked. This fix was introduced in commit `be4c7e23c6` and has been included in releases 6.2.2 and 5.5.5. Users are advised to upgrade. Users unable to upgrade should make use of parse server's security layers to manage access levels with Class-Level Permissions and Object-Level Access Control that should be used instead of custom security layers in Cloud Code triggers.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-670 \u2014 Always-Incorrect Control Flow Implementation
The code contains a control flow path that does not reflect the algorithm that the path is intended to implement, leading to incorrect behavior any time this path is navigated.
Repository: https://github.com/parse-community/parse-server
Files changed:
spec/CloudCode.spec.js
spec/ParseGraphQLServer.spec.js
spec/ParseRole.spec.js
spec/RestQuery.spec.js
spec/rest.spec.js
src/Auth.js
src/Controllers/PushController.js
src/Controllers/UserController.js
src/RestQuery.js
src/RestWrite.js
src/SharedRest.js
src/rest.js
Commit message: fix: Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger; fixes security vulnerability [GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q)



Vulnerable code:

    const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
    const query = new RestQuery(
      auth.master(config),
      'Object',
      { value: { $gt: 2 } },
      { limit: 2 }
    );
    const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
    const queryOne = new RestQuery(
      auth.master(config),
      'Letter',
      {
      { limit: 1 }
    );
    const queryTwo = new RestQuery(
      auth.master(config),
      'Letter',
      {
      { limit: 1 }
    );
    const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
    const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) {
  var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
    await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
      results.push(result)
    );
    await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
      results.push(result)
    );
      badgeUpdate = () => {
        const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
  verifyEmail(username, token) {
    var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', {
      username,
  getUserIfNeeded(user) {
    var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
function RestQuery(
RestQuery.prototype.execute = function (executeOptions) {
RestQuery.prototype.each = function (callback) {
      const query = new RestQuery(
RestQuery.prototype.buildRestWhere = function () {
RestQuery.prototype.getUserAndRoleACL = function () {
RestQuery.prototype.redirectClassNameForKey = function () {
RestQuery.prototype.validateClientClassCreation = function () {
RestQuery.prototype.replaceInQuery = function () {
  var subquery = new RestQuery(
    this.config,
    this.auth,
    inQueryValue.className,
    inQueryValue.where,
    additionalOptions
  );
RestQuery.prototype.replaceNotInQuery = function () {
  var subquery = new RestQuery(
    this.config,
    this.auth,
    notInQueryValue.className,
    notInQueryValue.where,
    additionalOptions
  );
RestQuery.prototype.replaceSelect = function () {
  var subquery = new RestQuery(
    this.config,
    this.auth,
    selectValue.query.className,
    selectValue.query.where,
    additionalOptions
  );
RestQuery.prototype.replaceDontSelect = function () {
  var subquery = new RestQuery(
    this.config,
    this.auth,
    dontSelectValue.query.className,
    dontSelectValue.query.where,
    additionalOptions
  );
RestQuery.prototype.cleanResultAuthData = function (result) {
RestQuery.prototype.replaceEquality = function () {
RestQuery.prototype.runFind = function (options = {}) {
RestQuery.prototype.runCount = function () {
RestQuery.prototype.denyProtectedFields = async function () {
RestQuery.prototype.handleIncludeAll = function () {
RestQuery.prototype.handleExcludeKeys = function () {
RestQuery.prototype.handleInclude = function () {
RestQuery.prototype.runAfterFindTrigger = function () {
RestQuery.prototype.handleAuthAdapters = async function () {
  const queryPromises = Object.keys(pointersHash).map(className => {
    var query = new RestQuery(config, auth, className, where, includeRestOptions);
RestWrite.prototype.transformUser = function () {
    promise = new RestQuery(this.config, Auth.master(this.config), '_Session', {
      user: {
        __type: 'Pointer',
        className: '_User',
        objectId: this.objectId(),
    })
      .execute()
      .then(results => {
        results.results.forEach(session =>
          this.config.cacheController.user.del(session.sessionToken)
        );
      });
function find(config, auth, className, restWhere, restOptions, clientSDK, context) {
  enforceRoleSecurity('find', className, auth);
  return triggers
    .maybeRunQueryTrigger(
      triggers.Types.beforeFind,
      className,
      restWhere,
      restOptions,
      config,
      auth,
      context
    )
    .then(result => {
      restWhere = result.restWhere || restWhere;
      restOptions = result.restOptions || restOptions;
      const query = new RestQuery(
        config,
        auth,
        className,
        restWhere,
        restOptions,
        clientSDK,
        true,
        context
      );
      return query.execute();
    });
}
const get = (config, auth, className, objectId, restOptions, clientSDK, context) => {
  enforceRoleSecurity('get', className, auth);
  return triggers
    .maybeRunQueryTrigger(
      triggers.Types.beforeFind,
      className,
      restWhere,
      restOptions,
      config,
      auth,
      context,
      true
    )
    .then(result => {
      restWhere = result.restWhere || restWhere;
      restOptions = result.restOptions || restOptions;
      const query = new RestQuery(
        config,
        auth,
        className,
        restWhere,
        restOptions,
        clientSDK,
        true,
        context
      );
      return query.execute();
    });
    .then(() => {
        return new RestQuery(config, auth, className, { objectId })
          .execute({ op: 'delete' })
          .then(response => {
            if (response && response.results && response.results.length) {
              const firstResult = response.results[0];
              firstResult.className = className;
              if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
                if (!auth.user || firstResult.user.objectId !== auth.user.id) {
                  throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
                }
              var cacheAdapter = config.cacheController;
              cacheAdapter.user.del(firstResult.sessionToken);
              inflatedObject = Parse.Object.fromJSON(firstResult);
              return triggers.maybeRunTrigger(
                triggers.Types.beforeDelete,
                auth,
                inflatedObject,
                null,
                config,
                context
              );
            throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.');
          });
    .then(() => {
        return new RestQuery(
          undefined,
          undefined,
          false,
          context
        ).execute({
const classesWithMasterOnlyAccess = [
  '_JobStatus',
  '_PushStatus',
  '_Hooks',
  '_GlobalConfig',
  '_JobSchedule',
  '_Idempotency',
];
// Disallowing access to the _Role collection except by master key
function enforceRoleSecurity(method, className, auth) {
  if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
    if (method === 'delete' || method === 'find') {
      const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
      throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
    }
  }

  //all volatileClasses are masterKey only
  if (
    classesWithMasterOnlyAccess.indexOf(className) >= 0 &&
    !auth.isMaster &&
    !auth.isMaintenance
  ) {
    const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
    throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
  }

  // readOnly masterKey is not allowed
  if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
    const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
    throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
  }
}


Fixed code:

  it('should run beforeFind on pointers and array of pointers from an object', async () => {
    const obj1 = new Parse.Object('TestObject');
    const obj2 = new Parse.Object('TestObject2');
    const obj3 = new Parse.Object('TestObject');
    obj2.set('aField', 'aFieldValue');
    await obj2.save();
    obj1.set('pointerField', obj2);
    obj3.set('pointerFieldArray', [obj2]);
    await obj1.save();
    await obj3.save();
    const spy = jasmine.createSpy('beforeFindSpy');
    Parse.Cloud.beforeFind('TestObject2', spy);
    const query = new Parse.Query('TestObject');
    await query.get(obj1.id);
    // Pointer not included in query so we don't expect beforeFind to be called
    expect(spy).not.toHaveBeenCalled();
    const query2 = new Parse.Query('TestObject');
    query2.include('pointerField');
    const res = await query2.get(obj1.id);
    expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
    // Pointer included in query so we expect beforeFind to be called
    expect(spy).toHaveBeenCalledTimes(1);
    const query3 = new Parse.Query('TestObject');
    query3.include('pointerFieldArray');
    const res2 = await query3.get(obj3.id);
    expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
    expect(spy).toHaveBeenCalledTimes(2);
  });
    const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
    const query = await RestQuery({
      method: RestQuery.Method.find,
      auth: auth.master(config),
      className: 'Object',
      restWhere: { value: { $gt: 2 } },
      restOptions: { limit: 2 },
    });
    const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
    const queryOne = await RestQuery({
      method: RestQuery.Method.get,
      auth: auth.master(config),
      className: 'Letter',
      restWhere: {
      restOptions: { limit: 1 },
    });

    const queryTwo = await RestQuery({
      method: RestQuery.Method.get,
      auth: auth.master(config),
      className: 'Letter',
      restWhere: {
      restOptions: { limit: 1 },
    });
    const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
  it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
    const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
    await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
    const obj2 = new Parse.Object('TestObject');
    // Anyone is can basically create a pointer to any object
    // or some developers can use master key in some hook to link
    // private objects to standard objects
    obj2.set('pointer', masterKeyOnlyClassObject);
    await obj2.save();
    const query = new Parse.Query('TestObject');
    query.include('pointer');
    await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
      "Clients aren't allowed to perform the get operation on the _PushStatus collection."
    );
  });

  it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
    await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
    const obj2 = new Parse.Object('TestObject');
    obj2.set('globalConfigPointer', {
      __type: 'Pointer',
      className: '_GlobalConfig',
      objectId: 1,
    });
    await obj2.save();
    const query = new Parse.Query('TestObject');
    query.include('globalConfigPointer');
    await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
      "Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
    );
  });

    const query = await RestQuery({
      method: RestQuery.Method.get,
      config,
      runBeforeFind: false,
      auth: master(config),
      className: '_Session',
      restWhere: { sessionToken },
      restOptions,
    });
var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
  var query = await RestQuery({
    method: RestQuery.Method.get,
    config,
    runBeforeFind: false,
    auth: master(config),
    className: '_User',
    restWhere: { _session_token: sessionToken },
    restOptions,
  });
    const query = await RestQuery({
      method: RestQuery.Method.find,
      runBeforeFind: false,
      config: this.config,
      auth: master(this.config),
      className: '_Role',
      restWhere,
    });
    await query.each(result => results.push(result));
    const query = await RestQuery({
      method: RestQuery.Method.find,
      config: this.config,
      runBeforeFind: false,
      auth: master(this.config),
      className: '_Role',
      restWhere,
    });
    await query.each(result => results.push(result));
      badgeUpdate = async () => {
        const restQuery = await RestQuery({
          method: RestQuery.Method.find,
          config,
          runBeforeFind: false,
          auth: master(config),
          className: '_Installation',
          restWhere: updateWhere,
        });
  async verifyEmail(username, token) {
    var findUserForEmailVerification = await RestQuery({
      method: RestQuery.Method.get,
      config: this.config,
      auth: maintenanceAuth,
      className: '_User',
      restWhere: {
        username,
      },
  async getUserIfNeeded(user) {
    var query = await RestQuery({
      method: RestQuery.Method.get,
      config: this.config,
      runBeforeFind: false,
      auth: Auth.master(this.config),
      className: '_User',
      restWhere: where,
    });
const { enforceRoleSecurity } = require('./SharedRest');

/**
 * Use to perform a query on a class. It will run security checks and triggers.
 * @param options
 * @param options.method {RestQuery.Method} The type of query to perform
 * @param options.config {ParseServerConfiguration} The server configuration
 * @param options.auth {Auth} The auth object for the request
 * @param options.className {string} The name of the class to query
 * @param options.restWhere {object} The where object for the query
 * @param options.restOptions {object} The options object for the query
 * @param options.clientSDK {string} The client SDK that is performing the query
 * @param options.runAfterFind {boolean} Whether to run the afterFind trigger
 * @param options.runBeforeFind {boolean} Whether to run the beforeFind trigger
 * @param options.context {object} The context object for the query
 * @returns {Promise<_UnsafeRestQuery>} A promise that is resolved with the _UnsafeRestQuery object
 */
async function RestQuery({
  method,
  config,
  auth,
  className,
  restWhere = {},
  restOptions = {},
  clientSDK,
  runAfterFind = true,
  runBeforeFind = true,
  context,
}) {
  if (![RestQuery.Method.find, RestQuery.Method.get].includes(method)) {
    throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
  }
  enforceRoleSecurity(method, className, auth);
  const result = runBeforeFind
    ? await triggers.maybeRunQueryTrigger(
      triggers.Types.beforeFind,
      className,
      restWhere,
      restOptions,
      config,
      auth,
      context,
      method === RestQuery.Method.get
    )
    : Promise.resolve({ restWhere, restOptions });

  return new _UnsafeRestQuery(
    config,
    auth,
    className,
    result.restWhere || restWhere,
    result.restOptions || restOptions,
    clientSDK,
    runAfterFind,
    context
  );
}

RestQuery.Method = Object.freeze({
  get: 'get',
  find: 'find',
});

/**
 * _UnsafeRestQuery is meant for specific internal usage only. When you need to skip security checks or some triggers.
 * Don't use it if you don't know what you are doing.
 * @param config
 * @param auth
 * @param className
 * @param restWhere
 * @param restOptions
 * @param clientSDK
 * @param runAfterFind
 * @param context
 */
function _UnsafeRestQuery(
_UnsafeRestQuery.prototype.execute = function (executeOptions) {
_UnsafeRestQuery.prototype.each = function (callback) {
      // Safe here to use _UnsafeRestQuery because the security was already
      // checked during "await RestQuery()"
      const query = new _UnsafeRestQuery(
_UnsafeRestQuery.prototype.buildRestWhere = function () {
_UnsafeRestQuery.prototype.getUserAndRoleACL = function () {
_UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
_UnsafeRestQuery.prototype.validateClientClassCreation = function () {
_UnsafeRestQuery.prototype.replaceInQuery = async function () {
  const subquery = await RestQuery({
    method: RestQuery.Method.find,
    config: this.config,
    auth: this.auth,
    className: inQueryValue.className,
    restWhere: inQueryValue.where,
    restOptions: additionalOptions,
  });
_UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
  const subquery = await RestQuery({
    method: RestQuery.Method.find,
    config: this.config,
    auth: this.auth,
    className: notInQueryValue.className,
    restWhere: notInQueryValue.where,
    restOptions: additionalOptions,
  });

_UnsafeRestQuery.prototype.replaceSelect = async function () {
  const subquery = await RestQuery({
    method: RestQuery.Method.find,
    config: this.config,
    auth: this.auth,
    className: selectValue.query.className,
    restWhere: selectValue.query.where,
    restOptions: additionalOptions,
  });

_UnsafeRestQuery.prototype.replaceDontSelect = async function () {
  const subquery = await RestQuery({
    method: RestQuery.Method.find,
    config: this.config,
    auth: this.auth,
    className: dontSelectValue.query.className,
    restWhere: dontSelectValue.query.where,
    restOptions: additionalOptions,
  });

_UnsafeRestQuery.prototype.cleanResultAuthData = function (result) {
_UnsafeRestQuery.prototype.replaceEquality = function () {
_UnsafeRestQuery.prototype.runFind = function (options = {}) {
_UnsafeRestQuery.prototype.runCount = function () {
_UnsafeRestQuery.prototype.denyProtectedFields = async function () {
_UnsafeRestQuery.prototype.handleIncludeAll = function () {
_UnsafeRestQuery.prototype.handleExcludeKeys = function () {
_UnsafeRestQuery.prototype.handleInclude = function () {
_UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
_UnsafeRestQuery.prototype.handleAuthAdapters = async function () {
  const queryPromises = Object.keys(pointersHash).map(async className => {
    const query = await RestQuery({
      method: objectIds.length === 1 ? RestQuery.Method.get : RestQuery.Method.find,
      config,
      auth,
      className,
      restWhere: where,
      restOptions: includeRestOptions,
    });
// For tests
module.exports._UnsafeRestQuery = _UnsafeRestQuery;
RestWrite.prototype.transformUser = async function () {
    const query = await RestQuery({
      method: RestQuery.Method.find,
      config: this.config,
      auth: Auth.master(this.config),
      className: '_Session',
      runBeforeFind: false,
      restWhere: {
        user: {
          __type: 'Pointer',
          className: '_User',
          objectId: this.objectId(),
        },
    });
    promise = query.execute().then(results => {
      results.results.forEach(session =>
        this.config.cacheController.user.del(session.sessionToken)
      );
    });
const classesWithMasterOnlyAccess = [
  '_JobStatus',
  '_PushStatus',
  '_Hooks',
  '_GlobalConfig',
  '_JobSchedule',
  '_Idempotency',
];
// Disallowing access to the _Role collection except by master key
function enforceRoleSecurity(method, className, auth) {
  if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) {
    if (method === 'delete' || method === 'find') {
      const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`;
      throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
    }
  }

  //all volatileClasses are masterKey only
  if (
    classesWithMasterOnlyAccess.indexOf(className) >= 0 &&
    !auth.isMaster &&
    !auth.isMaintenance
  ) {
    const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`;
    throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
  }

  // readOnly masterKey is not allowed
  if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
    const error = `read-only masterKey isn't allowed to perform the ${method} operation.`;
    throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error);
  }
}

module.exports = {
  enforceRoleSecurity,
};
const { enforceRoleSecurity } = require('./SharedRest');
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
  const query = await RestQuery({
    method: RestQuery.Method.find,
    config,
    auth,
    className,
    restWhere,
    restOptions,
    clientSDK,
    context,
  });
  return query.execute();
};
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
  const query = await RestQuery({
    method: RestQuery.Method.get,
    config,
    auth,
    className,
    restWhere,
    restOptions,
    clientSDK,
    context,
  });
  return query.execute();
    .then(async () => {
        const query = await RestQuery({
          method: RestQuery.Method.get,
          config,
          auth,
          className,
          restWhere: { objectId },
        });
        return query.execute({ op: 'delete' }).then(response => {
          if (response && response.results && response.results.length) {
            const firstResult = response.results[0];
            firstResult.className = className;
            if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) {
              if (!auth.user || firstResult.user.objectId !== auth.user.id) {
                throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
            var cacheAdapter = config.cacheController;
            cacheAdapter.user.del(firstResult.sessionToken);
            inflatedObject = Parse.Object.fromJSON(firstResult);
            return triggers.maybeRunTrigger(
              triggers.Types.beforeDelete,
              auth,
              inflatedObject,
              null,
              config,
              context
            );
          }
          throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.');
        });
    .then(async () => {
        const query = await RestQuery({
          method: RestQuery.Method.get,
          runAfterFind: false,
          runBeforeFind: false,
          context,
        });
        return query.execute({