CVE-2022-36079 (JavaScript, published 2022-09-07T21:15Z)
Description: value: Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Internal fields (keys used internally by Parse Server, prefixed by `_`) and protected fields (user defined) can be used as query constraints. Internal and protected fields are removed by Parse Server and are only returned to the client using a valid master key. However, using query constraints, these fields can be guessed by enumerating until Parse Server, prior to versions 4.10.14 or 5.2.5, returns a response object. The patch available in versions 4.10.14 and 5.2.5 requires the maser key to use internal and protected fields as query constraints. As a workaround, implement a Parse Cloud Trigger `beforeFind` and manually remove the query constraints.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/parse-community/parse-server
Files changed:
spec/RestQuery.spec.js
src/Controllers/DatabaseController.js
src/RestQuery.js
Commit message: fix: brute force guessing of user sensitive data via search patterns; this fixes a security vulnerability in which internal and protected fields may be used as query constraints to guess the value of these fields and obtain sensitive data (GHSA-2m6g-crv8-p3c6) (#8144)



Vulnerable code:
const specialQuerykeys = [
  '$and',
  '$or',
  '$nor',
  '_rperm',
  '_wperm',
  '_perishable_token',
  '_account_lockout_expires_at',
const isSpecialQueryKey = key => {
  return specialQuerykeys.indexOf(key) >= 0;
};

const validateQuery = (query: any): void => {
      query.$or.forEach(validateQuery);
      query.$and.forEach(validateQuery);
      query.$nor.forEach(validateQuery);
    if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
  if (!isUserClass) {
    return object;
  object.password = object._hashed_password;
  delete object._hashed_password;

  delete object.sessionToken;

  delete object._email_verify_token;
  delete object._perishable_token;
  delete object._perishable_token_expires_at;
  delete object._tombstone;
  delete object._email_verify_token_expires_at;
  delete object._failed_login_count;
  delete object._account_lockout_expires_at;
  delete object._password_changed_at;
  delete object._password_history;
          validateQuery(query);
        validateQuery(query);
              validateQuery(query);
  static _validateQuery: any => void;

Fixed code:
  it('query internal field', async () => {
    const internalFields = [
      '_email_verify_token',
      '_perishable_token',
      '_tombstone',
      '_email_verify_token_expires_at',
      '_failed_login_count',
      '_account_lockout_expires_at',
      '_password_changed_at',
      '_password_history',
    ];
    await Promise.all([
      ...internalFields.map(field =>
        expectAsync(new Parse.Query(Parse.User).exists(field).find()).toBeRejectedWith(
          new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${field}`)
        )
      ),
      ...internalFields.map(field =>
        new Parse.Query(Parse.User).exists(field).find({ useMasterKey: true })
      ),
    ]);
  });

  it('query protected field', async () => {
    const user = new Parse.User();
    user.setUsername('username1');
    user.setPassword('password');
    await user.signUp();
    const config = Config.get(Parse.applicationId);
    const obj = new Parse.Object('Test');

    obj.set('owner', user);
    obj.set('test', 'test');
    obj.set('zip', 1234);
    await obj.save();

    const schema = await config.database.loadSchema();
    await schema.updateClass(
      'Test',
      {},
      {
        get: { '*': true },
        find: { '*': true },
        protectedFields: { [user.id]: ['zip'] },
      }
    );
    await Promise.all([
      new Parse.Query('Test').exists('test').find(),
      expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith(
        new Parse.Error(
          Parse.Error.OPERATION_FORBIDDEN,
          'This user is not allowed to query zip on class Test'
        )
      ),
    ]);
  });

  it('query protected field with matchesQuery', async () => {
    const user = new Parse.User();
    user.setUsername('username1');
    user.setPassword('password');
    await user.signUp();
    const test = new Parse.Object('TestObject', { user });
    await test.save();
    const subQuery = new Parse.Query(Parse.User);
    subQuery.exists('_perishable_token');
    await expectAsync(
      new Parse.Query('TestObject').matchesQuery('user', subQuery).find()
    ).toBeRejectedWith(
      new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: _perishable_token')
    );
  });

const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
const specialMasterQueryKeys = [
  ...specialQueryKeys,
  '_perishable_token',
  '_tombstone',
  '_account_lockout_expires_at',
  '_password_changed_at',
  '_password_history',
const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
      query.$or.forEach(value => validateQuery(value, isMaster, update));
      query.$and.forEach(value => validateQuery(value, isMaster, update));
      query.$nor.forEach(value => validateQuery(value, isMaster, update));
    if (
      !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) &&
      ((!specialQueryKeys.includes(key) && !isMaster && !update) ||
        (update && isMaster && !specialMasterQueryKeys.includes(key)))
    ) {
  if (isUserClass) {
    object.password = object._hashed_password;
    delete object._hashed_password;
    delete object.sessionToken;
  for (const key in object) {
    if (key.charAt(0) === '_') {
      delete object[key];
    }
  }

  if (!isUserClass) {
    return object;
  }
          validateQuery(query, isMaster, true);
        validateQuery(query, isMaster, false);
              validateQuery(query, isMaster, false);
  static _validateQuery: (any, boolean, boolean) => void;
    .then(() => {
      return this.denyProtectedFields();
    })
RestQuery.prototype.denyProtectedFields = async function () {
  if (this.auth.isMaster) {
    return;
  }
  const schemaController = await this.config.database.loadSchema();
  const protectedFields =
    this.config.database.addProtectedFields(
      schemaController,
      this.className,
      this.restWhere,
      this.findOptions.acl,
      this.auth,
      this.findOptions
    ) || [];
  for (const key of protectedFields) {
    if (this.restWhere[key]) {
      throw new Parse.Error(
        Parse.Error.OPERATION_FORBIDDEN,
        `This user is not allowed to query ${key} on class ${this.className}`
      );
    }
  }
};