VYPR
High severityNVD Advisory· Published Sep 7, 2022· Updated Apr 23, 2025

Parse Server vulnerable to brute force guessing of user sensitive data via search patterns

CVE-2022-36079

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
parse-servernpm
< 4.10.144.10.14
parse-servernpm
>= 5.0.0, < 5.2.55.2.5

Affected products

1

Patches

2
634c44acd18f

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) (#8143)

5 files changed · +139 40
  • .github/workflows/ci.yml+1 0 modified
    @@ -13,6 +13,7 @@ env:
     jobs:
       check-mongo:
         strategy:
    +      fail-fast: false
           matrix:
             include:
               - name: Mongo 4.0.4, ReplicaSet, WiredTiger
    
  • spec/RedisCacheAdapter.spec.js+4 4 modified
    @@ -378,7 +378,7 @@ describe_only(() => {
     
         const query = new Parse.Query(TestObject);
         await query.get(object.id);
    -    expect(getSpy.calls.count()).toBe(3);
    +    expect(getSpy.calls.count()).toBe(4);
         expect(putSpy.calls.count()).toBe(1);
         expect(delSpy.calls.count()).toBe(2);
     
    @@ -397,7 +397,7 @@ describe_only(() => {
     
         const query = new Parse.Query(TestObject);
         await query.get(object.id);
    -    expect(getSpy.calls.count()).toBe(2);
    +    expect(getSpy.calls.count()).toBe(3);
         expect(putSpy.calls.count()).toBe(1);
         expect(delSpy.calls.count()).toBe(1);
     
    @@ -420,7 +420,7 @@ describe_only(() => {
         query.include('child');
         await query.get(object.id);
     
    -    expect(getSpy.calls.count()).toBe(4);
    +    expect(getSpy.calls.count()).toBe(6);
         expect(putSpy.calls.count()).toBe(1);
         expect(delSpy.calls.count()).toBe(3);
     
    @@ -444,7 +444,7 @@ describe_only(() => {
         expect(objects.length).toBe(1);
         expect(objects[0].id).toBe(child.id);
     
    -    expect(getSpy.calls.count()).toBe(2);
    +    expect(getSpy.calls.count()).toBe(3);
         expect(putSpy.calls.count()).toBe(1);
         expect(delSpy.calls.count()).toBe(3);
     
    
  • spec/RestQuery.spec.js+73 0 modified
    @@ -191,6 +191,79 @@ describe('rest query', () => {
         expect(result.results.length).toEqual(0);
       });
     
    +  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')
    +    );
    +  });
    +
       it('query with wrongly encoded parameter', done => {
         rest
           .create(config, nobody, 'TestParameterEncode', { foo: 'bar' })
    
  • src/Controllers/DatabaseController.js+34 36 modified
    @@ -51,47 +51,43 @@ const transformObjectACL = ({ ACL, ...result }) => {
       return result;
     };
     
    -const specialQuerykeys = [
    -  '$and',
    -  '$or',
    -  '$nor',
    -  '_rperm',
    -  '_wperm',
    -  '_perishable_token',
    +const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
    +const specialMasterQueryKeys = [
    +  ...specialQueryKeys,
       '_email_verify_token',
    +  '_perishable_token',
    +  '_tombstone',
       '_email_verify_token_expires_at',
    -  '_account_lockout_expires_at',
       '_failed_login_count',
    +  '_account_lockout_expires_at',
    +  '_password_changed_at',
    +  '_password_history',
     ];
     
    -const isSpecialQueryKey = key => {
    -  return specialQuerykeys.indexOf(key) >= 0;
    -};
    -
    -const validateQuery = (query: any): void => {
    +const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
       if (query.ACL) {
         throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
       }
     
       if (query.$or) {
         if (query.$or instanceof Array) {
    -      query.$or.forEach(validateQuery);
    +      query.$or.forEach(value => validateQuery(value, isMaster, update));
         } else {
           throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
         }
       }
     
       if (query.$and) {
         if (query.$and instanceof Array) {
    -      query.$and.forEach(validateQuery);
    +      query.$and.forEach(value => validateQuery(value, isMaster, update));
         } else {
           throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
         }
       }
     
       if (query.$nor) {
         if (query.$nor instanceof Array && query.$nor.length > 0) {
    -      query.$nor.forEach(validateQuery);
    +      query.$nor.forEach(value => validateQuery(value, isMaster, update));
         } else {
           throw new Parse.Error(
             Parse.Error.INVALID_QUERY,
    @@ -111,7 +107,11 @@ const validateQuery = (query: any): void => {
             }
           }
         }
    -    if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
    +    if (
    +      !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) &&
    +      ((!specialQueryKeys.includes(key) && !isMaster && !update) ||
    +        (update && isMaster && !specialMasterQueryKeys.includes(key)))
    +    ) {
           throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
         }
       });
    @@ -204,27 +204,25 @@ const filterSensitiveData = (
           perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
       }
     
    -  if (!isUserClass) {
    -    return object;
    +  if (isUserClass) {
    +    object.password = object._hashed_password;
    +    delete object._hashed_password;
    +    delete object.sessionToken;
       }
     
    -  object.password = object._hashed_password;
    -  delete object._hashed_password;
    +  if (isMaster) {
    +    return object;
    +  }
     
    -  delete object.sessionToken;
    +  for (const key in object) {
    +    if (key.charAt(0) === '_') {
    +      delete object[key];
    +    }
    +  }
     
    -  if (isMaster) {
    +  if (!isUserClass) {
         return object;
       }
    -  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;
     
       if (aclGroup.indexOf(object.objectId) > -1) {
         return object;
    @@ -513,7 +511,7 @@ class DatabaseController {
               if (acl) {
                 query = addWriteACL(query, acl);
               }
    -          validateQuery(query);
    +          validateQuery(query, isMaster, true);
               return schemaController
                 .getOneSchema(className, true)
                 .catch(error => {
    @@ -759,7 +757,7 @@ class DatabaseController {
             if (acl) {
               query = addWriteACL(query, acl);
             }
    -        validateQuery(query);
    +        validateQuery(query, isMaster, false);
             return schemaController
               .getOneSchema(className)
               .catch(error => {
    @@ -1232,7 +1230,7 @@ class DatabaseController {
                       query = addReadACL(query, aclGroup);
                     }
                   }
    -              validateQuery(query);
    +              validateQuery(query, isMaster, false);
                   if (count) {
                     if (!classExists) {
                       return 0;
    @@ -1744,7 +1742,7 @@ class DatabaseController {
         return Promise.resolve(response);
       }
     
    -  static _validateQuery: any => void;
    +  static _validateQuery: (any, boolean, boolean) => void;
       static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
     }
     
    
  • src/RestQuery.js+27 0 modified
    @@ -187,6 +187,9 @@ RestQuery.prototype.execute = function (executeOptions) {
         .then(() => {
           return this.buildRestWhere();
         })
    +    .then(() => {
    +      return this.denyProtectedFields();
    +    })
         .then(() => {
           return this.handleIncludeAll();
         })
    @@ -664,6 +667,30 @@ RestQuery.prototype.runCount = function () {
       });
     };
     
    +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}`
    +      );
    +    }
    +  }
    +};
    +
     // Augments this.response with all pointers on an object
     RestQuery.prototype.handleIncludeAll = function () {
       if (!this.includeAll) {
    
e39d51bd329c

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)

3 files changed · +134 37
  • spec/RestQuery.spec.js+73 0 modified
    @@ -191,6 +191,79 @@ describe('rest query', () => {
         expect(result.results.length).toEqual(0);
       });
     
    +  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')
    +    );
    +  });
    +
       it('query with wrongly encoded parameter', done => {
         rest
           .create(config, nobody, 'TestParameterEncode', { foo: 'bar' })
    
  • src/Controllers/DatabaseController.js+34 37 modified
    @@ -55,47 +55,43 @@ const transformObjectACL = ({ ACL, ...result }) => {
       return result;
     };
     
    -const specialQuerykeys = [
    -  '$and',
    -  '$or',
    -  '$nor',
    -  '_rperm',
    -  '_wperm',
    -  '_perishable_token',
    +const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
    +const specialMasterQueryKeys = [
    +  ...specialQueryKeys,
       '_email_verify_token',
    +  '_perishable_token',
    +  '_tombstone',
       '_email_verify_token_expires_at',
    -  '_account_lockout_expires_at',
       '_failed_login_count',
    +  '_account_lockout_expires_at',
    +  '_password_changed_at',
    +  '_password_history',
     ];
     
    -const isSpecialQueryKey = key => {
    -  return specialQuerykeys.indexOf(key) >= 0;
    -};
    -
    -const validateQuery = (query: any): void => {
    +const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
       if (query.ACL) {
         throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
       }
     
       if (query.$or) {
         if (query.$or instanceof Array) {
    -      query.$or.forEach(validateQuery);
    +      query.$or.forEach(value => validateQuery(value, isMaster, update));
         } else {
           throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
         }
       }
     
       if (query.$and) {
         if (query.$and instanceof Array) {
    -      query.$and.forEach(validateQuery);
    +      query.$and.forEach(value => validateQuery(value, isMaster, update));
         } else {
           throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
         }
       }
     
       if (query.$nor) {
         if (query.$nor instanceof Array && query.$nor.length > 0) {
    -      query.$nor.forEach(validateQuery);
    +      query.$nor.forEach(value => validateQuery(value, isMaster, update));
         } else {
           throw new Parse.Error(
             Parse.Error.INVALID_QUERY,
    @@ -115,7 +111,11 @@ const validateQuery = (query: any): void => {
             }
           }
         }
    -    if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
    +    if (
    +      !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) &&
    +      ((!specialQueryKeys.includes(key) && !isMaster && !update) ||
    +        (update && isMaster && !specialMasterQueryKeys.includes(key)))
    +    ) {
           throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
         }
       });
    @@ -208,27 +208,24 @@ const filterSensitiveData = (
           perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
       }
     
    -  if (!isUserClass) {
    -    return object;
    +  if (isUserClass) {
    +    object.password = object._hashed_password;
    +    delete object._hashed_password;
    +    delete object.sessionToken;
       }
     
    -  object.password = object._hashed_password;
    -  delete object._hashed_password;
    -
    -  delete object.sessionToken;
    -
       if (isMaster) {
         return object;
       }
    -  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;
    +  for (const key in object) {
    +    if (key.charAt(0) === '_') {
    +      delete object[key];
    +    }
    +  }
    +
    +  if (!isUserClass) {
    +    return object;
    +  }
     
       if (aclGroup.indexOf(object.objectId) > -1) {
         return object;
    @@ -515,7 +512,7 @@ class DatabaseController {
               if (acl) {
                 query = addWriteACL(query, acl);
               }
    -          validateQuery(query);
    +          validateQuery(query, isMaster, true);
               return schemaController
                 .getOneSchema(className, true)
                 .catch(error => {
    @@ -761,7 +758,7 @@ class DatabaseController {
             if (acl) {
               query = addWriteACL(query, acl);
             }
    -        validateQuery(query);
    +        validateQuery(query, isMaster, false);
             return schemaController
               .getOneSchema(className)
               .catch(error => {
    @@ -1253,7 +1250,7 @@ class DatabaseController {
                       query = addReadACL(query, aclGroup);
                     }
                   }
    -              validateQuery(query);
    +              validateQuery(query, isMaster, false);
                   if (count) {
                     if (!classExists) {
                       return 0;
    @@ -1809,7 +1806,7 @@ class DatabaseController {
         return Promise.resolve(response);
       }
     
    -  static _validateQuery: any => void;
    +  static _validateQuery: (any, boolean, boolean) => void;
       static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
     }
     
    
  • src/RestQuery.js+27 0 modified
    @@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) {
         .then(() => {
           return this.buildRestWhere();
         })
    +    .then(() => {
    +      return this.denyProtectedFields();
    +    })
         .then(() => {
           return this.handleIncludeAll();
         })
    @@ -688,6 +691,30 @@ RestQuery.prototype.runCount = function () {
       });
     };
     
    +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}`
    +      );
    +    }
    +  }
    +};
    +
     // Augments this.response with all pointers on an object
     RestQuery.prototype.handleIncludeAll = function () {
       if (!this.includeAll) {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.