CVE-2023-41058: Always-Incorrect Control Flow Implementation (CWE-670)
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.
CWE description: 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.
CVSS v3 base score: 7.5
Published: 2023-09-04T23:15Z
Repository: https://github.com/parse-community/parse-server
Language: JavaScript
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 date: 2023-09-04 14:19:48 +0200
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)


Diff: diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js
index c02999ad5..50e404858 100644
--- a/spec/CloudCode.spec.js
+++ b/spec/CloudCode.spec.js
@@ -2379,10 +2379,39 @@ describe('beforeFind hooks', () => {
         query.equalTo('objectId', myObj.id);
         return Promise.all([query.get(myObj.id), query.first(), query.find()]);
       })
       .then(() => done());
   });
+
+  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);
+  });
 });
 
 describe('afterFind hooks', () => {
   it('should add afterFind trigger', done => {
     Parse.Cloud.afterFind('MyObject', req => {
diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js
index 87718da13..022fb99fd 100644
--- a/spec/ParseGraphQLServer.spec.js
+++ b/spec/ParseGraphQLServer.spec.js
@@ -5273,11 +5273,10 @@ describe('ParseGraphQLServer', () => {
             expect(result.data.find.count).toEqual(2);
           });
 
           it('should only count', async () => {
             await prepareData();
-
             await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
 
             const where = {
               someField: {
                 in: ['someValue1', 'someValue2', 'someValue3'],
diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js
index 47fed865f..31de5b661 100644
--- a/spec/ParseRole.spec.js
+++ b/spec/ParseRole.spec.js
@@ -140,11 +140,11 @@ describe('Parse Role testing', () => {
         });
       });
       return Promise.all(promises);
     };
 
-    const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
+    const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
 
     let user, auth, getAllRolesSpy;
     createTestUser()
       .then(newUser => {
         user = newUser;
diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js
index 24e22ac4f..023d3b479 100644
--- a/spec/RestQuery.spec.js
+++ b/spec/RestQuery.spec.js
@@ -397,19 +397,20 @@ describe('RestQuery.each', () => {
     while (objects.length != 10) {
       objects.push(new Parse.Object('Object', { value: objects.length }));
     }
     const config = Config.get('test');
     await Parse.Object.saveAll(objects);
-    const query = new RestQuery(
+    const query = await RestQuery({
+      method: RestQuery.Method.find,
       config,
-      auth.master(config),
-      'Object',
-      { value: { $gt: 2 } },
-      { limit: 2 }
-    );
+      auth: auth.master(config),
+      className: 'Object',
+      restWhere: { value: { $gt: 2 } },
+      restOptions: { limit: 2 },
+    });
     const spy = spyOn(query, 'execute').and.callThrough();
-    const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
+    const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
     const results = [];
     await query.each(result => {
       expect(result.value).toBeGreaterThan(2);
       results.push(result);
     });
@@ -436,38 +437,41 @@ describe('RestQuery.each', () => {
 
     /**
      * Two queries needed since objectId are sorted and we can't know which one
      * going to be the first and then skip by the $gt added by each
      */
-    const queryOne = new RestQuery(
+    const queryOne = await RestQuery({
+      method: RestQuery.Method.get,
       config,
-      auth.master(config),
-      'Letter',
-      {
+      auth: auth.master(config),
+      className: 'Letter',
+      restWhere: {
         numbers: {
           __type: 'Pointer',
           className: 'Number',
           objectId: object1.id,
         },
       },
-      { limit: 1 }
-    );
-    const queryTwo = new RestQuery(
+      restOptions: { limit: 1 },
+    });
+
+    const queryTwo = await RestQuery({
+      method: RestQuery.Method.get,
       config,
-      auth.master(config),
-      'Letter',
-      {
+      auth: auth.master(config),
+      className: 'Letter',
+      restWhere: {
         numbers: {
           __type: 'Pointer',
           className: 'Number',
           objectId: object2.id,
         },
       },
-      { limit: 1 }
-    );
+      restOptions: { limit: 1 },
+    });
 
-    const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
+    const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
     const resultsOne = [];
     const resultsTwo = [];
     await queryOne.each(result => {
       resultsOne.push(result);
     });
diff --git a/spec/rest.spec.js b/spec/rest.spec.js
index 02d2f5960..61a5c728e 100644
--- a/spec/rest.spec.js
+++ b/spec/rest.spec.js
@@ -658,10 +658,42 @@ describe('rest create', () => {
         expect(error.code).toEqual(119);
         done();
       });
   });
 
+  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."
+    );
+  });
+
   it('locks down session', done => {
     let currentUser;
     Parse.User.signUp('foo', 'bar')
       .then(user => {
         currentUser = user;
diff --git a/src/Auth.js b/src/Auth.js
index abd14391d..15179b014 100644
--- a/src/Auth.js
+++ b/src/Auth.js
@@ -95,11 +95,19 @@ const getAuthForSessionToken = async function ({
     const restOptions = {
       limit: 1,
       include: 'user',
     };
     const RestQuery = require('./RestQuery');
-    const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
+    const query = await RestQuery({
+      method: RestQuery.Method.get,
+      config,
+      runBeforeFind: false,
+      auth: master(config),
+      className: '_Session',
+      restWhere: { sessionToken },
+      restOptions,
+    });
     results = (await query.execute()).results;
   } else {
     results = (
       await new Parse.Query(Parse.Session)
         .limit(1)
@@ -132,16 +140,24 @@ const getAuthForSessionToken = async function ({
     installationId,
     user: userObject,
   });
 };
 
-var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) {
+var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
   var restOptions = {
     limit: 1,
   };
   const RestQuery = require('./RestQuery');
-  var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
+  var query = await RestQuery({
+    method: RestQuery.Method.get,
+    config,
+    runBeforeFind: false,
+    auth: master(config),
+    className: '_User',
+    restWhere: { _session_token: sessionToken },
+    restOptions,
+  });
   return query.execute().then(response => {
     var results = response.results;
     if (results.length !== 1) {
       throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token');
     }
@@ -182,13 +198,19 @@ Auth.prototype.getRolesForUser = async function () {
         className: '_User',
         objectId: this.user.id,
       },
     };
     const RestQuery = require('./RestQuery');
-    await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result =>
-      results.push(result)
-    );
+    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));
   } else {
     await new Parse.Query(Parse.Role)
       .equalTo('users', this.user)
       .each(result => results.push(result.toJSON()), { useMasterKey: true });
   }
@@ -276,13 +298,19 @@ Auth.prototype.getRolesByIds = async function (ins) {
         objectId: id,
       };
     });
     const restWhere = { roles: { $in: roles } };
     const RestQuery = require('./RestQuery');
-    await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).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));
   }
   return results;
 };
 
 // Given a list of roleIds, find all the parent roles, returns a promise with all names
diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js
index 1a5b9bf49..04fb5c4fd 100644
--- a/src/Controllers/PushController.js
+++ b/src/Controllers/PushController.js
@@ -56,13 +56,20 @@ export class PushController {
         throw "Invalid value for badge, expected number or 'Increment' or {increment: number}";
       }
 
       // Force filtering on only valid device tokens
       const updateWhere = applyDeviceTokenExists(where);
-      badgeUpdate = () => {
+      badgeUpdate = async () => {
         // Build a real RestQuery so we can use it in RestWrite
-        const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
+        const restQuery = await RestQuery({
+          method: RestQuery.Method.find,
+          config,
+          runBeforeFind: false,
+          auth: master(config),
+          className: '_Installation',
+          restWhere: updateWhere,
+        });
         return restQuery.buildRestWhere().then(() => {
           const write = new RestWrite(
             config,
             master(config),
             '_Installation',
diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js
index 6871add98..51bc98788 100644
--- a/src/Controllers/UserController.js
+++ b/src/Controllers/UserController.js
@@ -46,11 +46,11 @@ export class UserController extends AdaptableController {
         );
       }
     }
   }
 
-  verifyEmail(username, token) {
+  async verifyEmail(username, token) {
     if (!this.shouldVerifyEmails) {
       // Trying to verify email when not enabled
       // TODO: Better error here.
       throw undefined;
     }
@@ -68,12 +68,18 @@ export class UserController extends AdaptableController {
       query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) };
 
       updateFields._email_verify_token_expires_at = { __op: 'Delete' };
     }
     const maintenanceAuth = Auth.maintenance(this.config);
-    var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', {
-      username,
+    var findUserForEmailVerification = await RestQuery({
+      method: RestQuery.Method.get,
+      config: this.config,
+      auth: maintenanceAuth,
+      className: '_User',
+      restWhere: {
+        username,
+      },
     });
     return findUserForEmailVerification.execute().then(result => {
       if (result.results.length && result.results[0].emailVerified) {
         return Promise.resolve(result.results.length[0]);
       } else if (result.results.length) {
@@ -108,11 +114,11 @@ export class UserController extends AdaptableController {
         }
         return results[0];
       });
   }
 
-  getUserIfNeeded(user) {
+  async getUserIfNeeded(user) {
     if (user.username && user.email) {
       return Promise.resolve(user);
     }
     var where = {};
     if (user.username) {
@@ -120,11 +126,18 @@ export class UserController extends AdaptableController {
     }
     if (user.email) {
       where.email = user.email;
     }
 
-    var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
+    var query = await RestQuery({
+      method: RestQuery.Method.get,
+      config: this.config,
+      runBeforeFind: false,
+      auth: Auth.master(this.config),
+      className: '_User',
+      restWhere: where,
+    });
     return query.execute().then(function (result) {
       if (result.results.length != 1) {
         throw undefined;
       }
       return result.results[0];
diff --git a/src/RestQuery.js b/src/RestQuery.js
index fe3617eb1..538d87d4c 100644
--- a/src/RestQuery.js
+++ b/src/RestQuery.js
@@ -4,10 +4,12 @@
 var SchemaController = require('./Controllers/SchemaController');
 var Parse = require('parse/node').Parse;
 const triggers = require('./triggers');
 const { continueWhile } = require('parse/lib/node/promiseUtils');
 const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
+const { enforceRoleSecurity } = require('./SharedRest');
+
 // restOptions can include:
 //   skip
 //   limit
 //   order
 //   count
@@ -16,11 +18,84 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
 //   excludeKeys
 //   redirectClassNameForKey
 //   readPreference
 //   includeReadPreference
 //   subqueryReadPreference
-function RestQuery(
+/**
+ * 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(
   config,
   auth,
   className,
   restWhere = {},
   restOptions = {},
@@ -195,11 +270,11 @@ function RestQuery(
 // A convenient method to perform all the steps of processing a query
 // in order.
 // Returns a promise for the response - an object with optional keys
 // 'results' and 'count'.
 // TODO: consolidate the replaceX functions
-RestQuery.prototype.execute = function (executeOptions) {
+_UnsafeRestQuery.prototype.execute = function (executeOptions) {
   return Promise.resolve()
     .then(() => {
       return this.buildRestWhere();
     })
     .then(() => {
@@ -229,11 +304,11 @@ RestQuery.prototype.execute = function (executeOptions) {
     .then(() => {
       return this.response;
     });
 };
 
-RestQuery.prototype.each = function (callback) {
+_UnsafeRestQuery.prototype.each = function (callback) {
   const { config, auth, className, restWhere, restOptions, clientSDK } = this;
   // if the limit is set, use it
   restOptions.limit = restOptions.limit || 100;
   restOptions.order = 'objectId';
   let finished = false;
@@ -241,11 +316,13 @@ RestQuery.prototype.each = function (callback) {
   return continueWhile(
     () => {
       return !finished;
     },
     async () => {
-      const query = new RestQuery(
+      // Safe here to use _UnsafeRestQuery because the security was already
+      // checked during "await RestQuery()"
+      const query = new _UnsafeRestQuery(
         config,
         auth,
         className,
         restWhere,
         restOptions,
@@ -263,11 +340,11 @@ RestQuery.prototype.each = function (callback) {
       }
     }
   );
 };
 
-RestQuery.prototype.buildRestWhere = function () {
+_UnsafeRestQuery.prototype.buildRestWhere = function () {
   return Promise.resolve()
     .then(() => {
       return this.getUserAndRoleACL();
     })
     .then(() => {
@@ -292,11 +369,11 @@ RestQuery.prototype.buildRestWhere = function () {
       return this.replaceEquality();
     });
 };
 
 // Uses the Auth object to get the list of roles, adds the user id
-RestQuery.prototype.getUserAndRoleACL = function () {
+_UnsafeRestQuery.prototype.getUserAndRoleACL = function () {
   if (this.auth.isMaster) {
     return Promise.resolve();
   }
 
   this.findOptions.acl = ['*'];
@@ -311,11 +388,11 @@ RestQuery.prototype.getUserAndRoleACL = function () {
   }
 };
 
 // Changes the className if redirectClassNameForKey is set.
 // Returns a promise.
-RestQuery.prototype.redirectClassNameForKey = function () {
+_UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
   if (!this.redirectKey) {
     return Promise.resolve();
   }
 
   // We need to change the class name based on the schema
@@ -326,11 +403,11 @@ RestQuery.prototype.redirectClassNameForKey = function () {
       this.redirectClassName = newClassName;
     });
 };
 
 // Validates this operation against the allowClientClassCreation config.
-RestQuery.prototype.validateClientClassCreation = function () {
+_UnsafeRestQuery.prototype.validateClientClassCreation = function () {
   if (
     this.config.allowClientClassCreation === false &&
     !this.auth.isMaster &&
     SchemaController.systemClasses.indexOf(this.className) === -1
   ) {
@@ -369,11 +446,11 @@ function transformInQuery(inQueryObject, className, results) {
 
 // Replaces a $inQuery clause by running the subquery, if there is an
 // $inQuery clause.
 // The $inQuery clause turns into an $in with values that are just
 // pointers to the objects returned in the subquery.
-RestQuery.prototype.replaceInQuery = function () {
+_UnsafeRestQuery.prototype.replaceInQuery = async function () {
   var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery');
   if (!inQueryObject) {
     return;
   }
 
@@ -392,17 +469,18 @@ RestQuery.prototype.replaceInQuery = function () {
     additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference;
   } else if (this.restOptions.readPreference) {
     additionalOptions.readPreference = this.restOptions.readPreference;
   }
 
-  var subquery = new RestQuery(
-    this.config,
-    this.auth,
-    inQueryValue.className,
-    inQueryValue.where,
-    additionalOptions
-  );
+  const subquery = await RestQuery({
+    method: RestQuery.Method.find,
+    config: this.config,
+    auth: this.auth,
+    className: inQueryValue.className,
+    restWhere: inQueryValue.where,
+    restOptions: additionalOptions,
+  });
   return subquery.execute().then(response => {
     transformInQuery(inQueryObject, subquery.className, response.results);
     // Recurse to repeat
     return this.replaceInQuery();
   });
@@ -427,11 +505,11 @@ function transformNotInQuery(notInQueryObject, className, results) {
 
 // Replaces a $notInQuery clause by running the subquery, if there is an
 // $notInQuery clause.
 // The $notInQuery clause turns into a $nin with values that are just
 // pointers to the objects returned in the subquery.
-RestQuery.prototype.replaceNotInQuery = function () {
+_UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
   var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery');
   if (!notInQueryObject) {
     return;
   }
 
@@ -450,17 +528,19 @@ RestQuery.prototype.replaceNotInQuery = function () {
     additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference;
   } else if (this.restOptions.readPreference) {
     additionalOptions.readPreference = this.restOptions.readPreference;
   }
 
-  var subquery = new RestQuery(
-    this.config,
-    this.auth,
-    notInQueryValue.className,
-    notInQueryValue.where,
-    additionalOptions
-  );
+  const subquery = await RestQuery({
+    method: RestQuery.Method.find,
+    config: this.config,
+    auth: this.auth,
+    className: notInQueryValue.className,
+    restWhere: notInQueryValue.where,
+    restOptions: additionalOptions,
+  });
+
   return subquery.execute().then(response => {
     transformNotInQuery(notInQueryObject, subquery.className, response.results);
     // Recurse to repeat
     return this.replaceNotInQuery();
   });
@@ -490,11 +570,11 @@ const transformSelect = (selectObject, key, objects) => {
 // Replaces a $select clause by running the subquery, if there is a
 // $select clause.
 // The $select clause turns into an $in with values selected out of
 // the subquery.
 // Returns a possible-promise.
-RestQuery.prototype.replaceSelect = function () {
+_UnsafeRestQuery.prototype.replaceSelect = async function () {
   var selectObject = findObjectWithKey(this.restWhere, '$select');
   if (!selectObject) {
     return;
   }
 
@@ -520,17 +600,19 @@ RestQuery.prototype.replaceSelect = function () {
     additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference;
   } else if (this.restOptions.readPreference) {
     additionalOptions.readPreference = this.restOptions.readPreference;
   }
 
-  var subquery = new RestQuery(
-    this.config,
-    this.auth,
-    selectValue.query.className,
-    selectValue.query.where,
-    additionalOptions
-  );
+  const subquery = await RestQuery({
+    method: RestQuery.Method.find,
+    config: this.config,
+    auth: this.auth,
+    className: selectValue.query.className,
+    restWhere: selectValue.query.where,
+    restOptions: additionalOptions,
+  });
+
   return subquery.execute().then(response => {
     transformSelect(selectObject, selectValue.key, response.results);
     // Keep replacing $select clauses
     return this.replaceSelect();
   });
@@ -552,11 +634,11 @@ const transformDontSelect = (dontSelectObject, key, objects) => {
 // Replaces a $dontSelect clause by running the subquery, if there is a
 // $dontSelect clause.
 // The $dontSelect clause turns into an $nin with values selected out of
 // the subquery.
 // Returns a possible-promise.
-RestQuery.prototype.replaceDontSelect = function () {
+_UnsafeRestQuery.prototype.replaceDontSelect = async function () {
   var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect');
   if (!dontSelectObject) {
     return;
   }
 
@@ -580,25 +662,27 @@ RestQuery.prototype.replaceDontSelect = function () {
     additionalOptions.subqueryReadPreference = this.restOptions.subqueryReadPreference;
   } else if (this.restOptions.readPreference) {
     additionalOptions.readPreference = this.restOptions.readPreference;
   }
 
-  var subquery = new RestQuery(
-    this.config,
-    this.auth,
-    dontSelectValue.query.className,
-    dontSelectValue.query.where,
-    additionalOptions
-  );
+  const subquery = await RestQuery({
+    method: RestQuery.Method.find,
+    config: this.config,
+    auth: this.auth,
+    className: dontSelectValue.query.className,
+    restWhere: dontSelectValue.query.where,
+    restOptions: additionalOptions,
+  });
+
   return subquery.execute().then(response => {
     transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
     // Keep replacing $dontSelect clauses
     return this.replaceDontSelect();
   });
 };
 
-RestQuery.prototype.cleanResultAuthData = function (result) {
+_UnsafeRestQuery.prototype.cleanResultAuthData = function (result) {
   delete result.password;
   if (result.authData) {
     Object.keys(result.authData).forEach(provider => {
       if (result.authData[provider] === null) {
         delete result.authData[provider];
@@ -633,22 +717,22 @@ const replaceEqualityConstraint = constraint => {
     });
   }
   return constraint;
 };
 
-RestQuery.prototype.replaceEquality = function () {
+_UnsafeRestQuery.prototype.replaceEquality = function () {
   if (typeof this.restWhere !== 'object') {
     return;
   }
   for (const key in this.restWhere) {
     this.restWhere[key] = replaceEqualityConstraint(this.restWhere[key]);
   }
 };
 
 // Returns a promise for whether it was successful.
 // Populates this.response with an object that only has 'results'.
-RestQuery.prototype.runFind = function (options = {}) {
+_UnsafeRestQuery.prototype.runFind = function (options = {}) {
   if (this.findOptions.limit === 0) {
     this.response = { results: [] };
     return Promise.resolve();
   }
   const findOptions = Object.assign({}, this.findOptions);
@@ -680,11 +764,11 @@ RestQuery.prototype.runFind = function (options = {}) {
     });
 };
 
 // Returns a promise for whether it was successful.
 // Populates this.response.count with the count
-RestQuery.prototype.runCount = function () {
+_UnsafeRestQuery.prototype.runCount = function () {
   if (!this.doCount) {
     return;
   }
   this.findOptions.count = true;
   delete this.findOptions.skip;
@@ -692,11 +776,11 @@ RestQuery.prototype.runCount = function () {
   return this.config.database.find(this.className, this.restWhere, this.findOptions).then(c => {
     this.response.count = c;
   });
 };
 
-RestQuery.prototype.denyProtectedFields = async function () {
+_UnsafeRestQuery.prototype.denyProtectedFields = async function () {
   if (this.auth.isMaster) {
     return;
   }
   const schemaController = await this.config.database.loadSchema();
   const protectedFields =
@@ -717,11 +801,11 @@ RestQuery.prototype.denyProtectedFields = async function () {
     }
   }
 };
 
 // Augments this.response with all pointers on an object
-RestQuery.prototype.handleIncludeAll = function () {
+_UnsafeRestQuery.prototype.handleIncludeAll = function () {
   if (!this.includeAll) {
     return;
   }
   return this.config.database
     .loadSchema()
@@ -746,11 +830,11 @@ RestQuery.prototype.handleIncludeAll = function () {
       }
     });
 };
 
 // Updates property `this.keys` to contain all keys but the ones unselected.
-RestQuery.prototype.handleExcludeKeys = function () {
+_UnsafeRestQuery.prototype.handleExcludeKeys = function () {
   if (!this.excludeKeys) {
     return;
   }
   if (this.keys) {
     this.keys = this.keys.filter(k => !this.excludeKeys.includes(k));
@@ -764,11 +848,11 @@ RestQuery.prototype.handleExcludeKeys = function () {
       this.keys = fields.filter(k => !this.excludeKeys.includes(k));
     });
 };
 
 // Augments this.response with data at the paths provided in this.include.
-RestQuery.prototype.handleInclude = function () {
+_UnsafeRestQuery.prototype.handleInclude = function () {
   if (this.include.length == 0) {
     return;
   }
 
   var pathResponse = includePath(
@@ -791,11 +875,11 @@ RestQuery.prototype.handleInclude = function () {
 
   return pathResponse;
 };
 
 //Returns a promise of a processed set of results
-RestQuery.prototype.runAfterFindTrigger = function () {
+_UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
   if (!this.response) {
     return;
   }
   if (!this.runAfterFind) {
     return;
@@ -843,11 +927,11 @@ RestQuery.prototype.runAfterFindTrigger = function () {
         this.response.results = results;
       }
     });
 };
 
-RestQuery.prototype.handleAuthAdapters = async function () {
+_UnsafeRestQuery.prototype.handleAuthAdapters = async function () {
   if (this.className !== '_User' || this.findOptions.explain) {
     return;
   }
   await Promise.all(
     this.response.results.map(result =>
@@ -925,19 +1009,26 @@ function includePath(config, auth, response, path, restOptions = {}) {
     includeRestOptions.includeReadPreference = restOptions.includeReadPreference;
   } else if (restOptions.readPreference) {
     includeRestOptions.readPreference = restOptions.readPreference;
   }
 
-  const queryPromises = Object.keys(pointersHash).map(className => {
+  const queryPromises = Object.keys(pointersHash).map(async className => {
     const objectIds = Array.from(pointersHash[className]);
     let where;
     if (objectIds.length === 1) {
       where = { objectId: objectIds[0] };
     } else {
       where = { objectId: { $in: objectIds } };
     }
-    var query = new RestQuery(config, auth, className, where, includeRestOptions);
+    const query = await RestQuery({
+      method: objectIds.length === 1 ? RestQuery.Method.get : RestQuery.Method.find,
+      config,
+      auth,
+      className,
+      restWhere: where,
+      restOptions: includeRestOptions,
+    });
     return query.execute({ op: 'get' }).then(results => {
       results.className = className;
       return Promise.resolve(results);
     });
   });
@@ -1064,5 +1155,7 @@ function findObjectWithKey(root, key) {
     }
   }
 }
 
 module.exports = RestQuery;
+// For tests
+module.exports._UnsafeRestQuery = _UnsafeRestQuery;
diff --git a/src/RestWrite.js b/src/RestWrite.js
index dd36eb378..31e2ddd23 100644
--- a/src/RestWrite.js
+++ b/src/RestWrite.js
@@ -601,11 +601,11 @@ RestWrite.prototype.handleAuthData = async function (authData) {
     }
   }
 };
 
 // The non-third-party parts of User transformation
-RestWrite.prototype.transformUser = function () {
+RestWrite.prototype.transformUser = async function () {
   var promise = Promise.resolve();
   if (this.className !== '_User') {
     return promise;
   }
 
@@ -616,23 +616,29 @@ RestWrite.prototype.transformUser = function () {
 
   // Do not cleanup session if objectId is not set
   if (this.query && this.objectId()) {
     // If we're updating a _User object, we need to clear out the cache for that user. Find all their
     // session tokens, and remove them from the cache.
-    promise = new RestQuery(this.config, Auth.master(this.config), '_Session', {
-      user: {
-        __type: 'Pointer',
-        className: '_User',
-        objectId: this.objectId(),
+    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(),
+        },
       },
-    })
-      .execute()
-      .then(results => {
-        results.results.forEach(session =>
-          this.config.cacheController.user.del(session.sessionToken)
-        );
-      });
+    });
+    promise = query.execute().then(results => {
+      results.results.forEach(session =>
+        this.config.cacheController.user.del(session.sessionToken)
+      );
+    });
   }
 
   return promise
     .then(() => {
       // Transform the password
diff --git a/src/SharedRest.js b/src/SharedRest.js
new file mode 100644
index 000000000..0b4a07c32
--- /dev/null
+++ b/src/SharedRest.js
@@ -0,0 +1,37 @@
+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,
+};
diff --git a/src/rest.js b/src/rest.js
index e1e53668a..1f9dbacb7 100644
--- a/src/rest.js
+++ b/src/rest.js
@@ -10,10 +10,11 @@
 var Parse = require('parse/node').Parse;
 
 var RestQuery = require('./RestQuery');
 var RestWrite = require('./RestWrite');
 var triggers = require('./triggers');
+const { enforceRoleSecurity } = require('./SharedRest');
 
 function checkTriggers(className, config, types) {
   return types.some(triggerType => {
     return triggers.getTrigger(className, triggers.Types[triggerType], config.applicationId);
   });
@@ -22,69 +23,38 @@ function checkTriggers(className, config, types) {
 function checkLiveQuery(className, config) {
   return config.liveQueryController && config.liveQueryController.hasLiveQuery(className);
 }
 
 // Returns a promise for an object with optional keys 'results' and 'count'.
-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 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();
+};
 
 // get is just like find but only queries an objectId.
-const get = (config, auth, className, objectId, restOptions, clientSDK, context) => {
+const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
   var restWhere = { objectId };
-  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();
-    });
+  const query = await RestQuery({
+    method: RestQuery.Method.get,
+    config,
+    auth,
+    className,
+    restWhere,
+    restOptions,
+    clientSDK,
+    context,
+  });
+  return query.execute();
 };
 
 // Returns a promise that doesn't resolve to any useful value.
 function del(config, auth, className, objectId, context) {
   if (typeof objectId !== 'string') {
@@ -99,39 +69,44 @@ function del(config, auth, className, objectId, context) {
 
   let inflatedObject;
   let schemaController;
 
   return Promise.resolve()
-    .then(() => {
+    .then(async () => {
       const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']);
       const hasLiveQuery = checkLiveQuery(className, config);
       if (hasTriggers || hasLiveQuery || className == '_Session') {
-        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');
-                }
+        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.');
-          });
+            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.');
+        });
       }
       return Promise.resolve({});
     })
     .then(() => {
       if (!auth.isMaster && !auth.isMaintenance) {
@@ -191,25 +166,26 @@ function create(config, auth, className, restObject, clientSDK, context) {
 // Usually, this is just updatedAt.
 function update(config, auth, className, restWhere, restObject, clientSDK, context) {
   enforceRoleSecurity('update', className, auth);
 
   return Promise.resolve()
-    .then(() => {
+    .then(async () => {
       const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']);
       const hasLiveQuery = checkLiveQuery(className, config);
       if (hasTriggers || hasLiveQuery) {
         // Do not use find, as it runs the before finds
-        return new RestQuery(
+        const query = await RestQuery({
+          method: RestQuery.Method.get,
           config,
           auth,
           className,
           restWhere,
-          undefined,
-          undefined,
-          false,
-          context
-        ).execute({
+          runAfterFind: false,
+          runBeforeFind: false,
+          context,
+        });
+        return query.execute({
           op: 'update',
         });
       }
       return Promise.resolve({});
     })
@@ -246,44 +222,10 @@ function handleSessionMissingError(error, className, auth) {
     throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.');
   }
   throw error;
 }
 
-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 = {
   create,
   del,
   find,
   get,

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({