Skip to content

Commit 4c0c7c7

Browse files
authored
fix: brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) (parse-community#8146) [skip release]
1 parent 5432082 commit 4c0c7c7

File tree

3 files changed

+134
-37
lines changed

3 files changed

+134
-37
lines changed

spec/RestQuery.spec.js

+73
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,79 @@ describe('rest query', () => {
191191
expect(result.results.length).toEqual(0);
192192
});
193193

194+
it('query internal field', async () => {
195+
const internalFields = [
196+
'_email_verify_token',
197+
'_perishable_token',
198+
'_tombstone',
199+
'_email_verify_token_expires_at',
200+
'_failed_login_count',
201+
'_account_lockout_expires_at',
202+
'_password_changed_at',
203+
'_password_history',
204+
];
205+
await Promise.all([
206+
...internalFields.map(field =>
207+
expectAsync(new Parse.Query(Parse.User).exists(field).find()).toBeRejectedWith(
208+
new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${field}`)
209+
)
210+
),
211+
...internalFields.map(field =>
212+
new Parse.Query(Parse.User).exists(field).find({ useMasterKey: true })
213+
),
214+
]);
215+
});
216+
217+
it('query protected field', async () => {
218+
const user = new Parse.User();
219+
user.setUsername('username1');
220+
user.setPassword('password');
221+
await user.signUp();
222+
const config = Config.get(Parse.applicationId);
223+
const obj = new Parse.Object('Test');
224+
225+
obj.set('owner', user);
226+
obj.set('test', 'test');
227+
obj.set('zip', 1234);
228+
await obj.save();
229+
230+
const schema = await config.database.loadSchema();
231+
await schema.updateClass(
232+
'Test',
233+
{},
234+
{
235+
get: { '*': true },
236+
find: { '*': true },
237+
protectedFields: { [user.id]: ['zip'] },
238+
}
239+
);
240+
await Promise.all([
241+
new Parse.Query('Test').exists('test').find(),
242+
expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith(
243+
new Parse.Error(
244+
Parse.Error.OPERATION_FORBIDDEN,
245+
'This user is not allowed to query zip on class Test'
246+
)
247+
),
248+
]);
249+
});
250+
251+
it('query protected field with matchesQuery', async () => {
252+
const user = new Parse.User();
253+
user.setUsername('username1');
254+
user.setPassword('password');
255+
await user.signUp();
256+
const test = new Parse.Object('TestObject', { user });
257+
await test.save();
258+
const subQuery = new Parse.Query(Parse.User);
259+
subQuery.exists('_perishable_token');
260+
await expectAsync(
261+
new Parse.Query('TestObject').matchesQuery('user', subQuery).find()
262+
).toBeRejectedWith(
263+
new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: _perishable_token')
264+
);
265+
});
266+
194267
it('query with wrongly encoded parameter', done => {
195268
rest
196269
.create(config, nobody, 'TestParameterEncode', { foo: 'bar' })

src/Controllers/DatabaseController.js

+34-37
Original file line numberDiff line numberDiff line change
@@ -55,47 +55,43 @@ const transformObjectACL = ({ ACL, ...result }) => {
5555
return result;
5656
};
5757

58-
const specialQuerykeys = [
59-
'$and',
60-
'$or',
61-
'$nor',
62-
'_rperm',
63-
'_wperm',
64-
'_perishable_token',
58+
const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
59+
const specialMasterQueryKeys = [
60+
...specialQueryKeys,
6561
'_email_verify_token',
62+
'_perishable_token',
63+
'_tombstone',
6664
'_email_verify_token_expires_at',
67-
'_account_lockout_expires_at',
6865
'_failed_login_count',
66+
'_account_lockout_expires_at',
67+
'_password_changed_at',
68+
'_password_history',
6969
];
7070

71-
const isSpecialQueryKey = key => {
72-
return specialQuerykeys.indexOf(key) >= 0;
73-
};
74-
75-
const validateQuery = (query: any): void => {
71+
const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
7672
if (query.ACL) {
7773
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
7874
}
7975

8076
if (query.$or) {
8177
if (query.$or instanceof Array) {
82-
query.$or.forEach(validateQuery);
78+
query.$or.forEach(value => validateQuery(value, isMaster, update));
8379
} else {
8480
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
8581
}
8682
}
8783

8884
if (query.$and) {
8985
if (query.$and instanceof Array) {
90-
query.$and.forEach(validateQuery);
86+
query.$and.forEach(value => validateQuery(value, isMaster, update));
9187
} else {
9288
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
9389
}
9490
}
9591

9692
if (query.$nor) {
9793
if (query.$nor instanceof Array && query.$nor.length > 0) {
98-
query.$nor.forEach(validateQuery);
94+
query.$nor.forEach(value => validateQuery(value, isMaster, update));
9995
} else {
10096
throw new Parse.Error(
10197
Parse.Error.INVALID_QUERY,
@@ -115,7 +111,11 @@ const validateQuery = (query: any): void => {
115111
}
116112
}
117113
}
118-
if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
114+
if (
115+
!key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) &&
116+
((!specialQueryKeys.includes(key) && !isMaster && !update) ||
117+
(update && isMaster && !specialMasterQueryKeys.includes(key)))
118+
) {
119119
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
120120
}
121121
});
@@ -208,27 +208,24 @@ const filterSensitiveData = (
208208
perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
209209
}
210210

211-
if (!isUserClass) {
212-
return object;
211+
if (isUserClass) {
212+
object.password = object._hashed_password;
213+
delete object._hashed_password;
214+
delete object.sessionToken;
213215
}
214216

215-
object.password = object._hashed_password;
216-
delete object._hashed_password;
217-
218-
delete object.sessionToken;
219-
220217
if (isMaster) {
221218
return object;
222219
}
223-
delete object._email_verify_token;
224-
delete object._perishable_token;
225-
delete object._perishable_token_expires_at;
226-
delete object._tombstone;
227-
delete object._email_verify_token_expires_at;
228-
delete object._failed_login_count;
229-
delete object._account_lockout_expires_at;
230-
delete object._password_changed_at;
231-
delete object._password_history;
220+
for (const key in object) {
221+
if (key.charAt(0) === '_') {
222+
delete object[key];
223+
}
224+
}
225+
226+
if (!isUserClass) {
227+
return object;
228+
}
232229

233230
if (aclGroup.indexOf(object.objectId) > -1) {
234231
return object;
@@ -515,7 +512,7 @@ class DatabaseController {
515512
if (acl) {
516513
query = addWriteACL(query, acl);
517514
}
518-
validateQuery(query);
515+
validateQuery(query, isMaster, true);
519516
return schemaController
520517
.getOneSchema(className, true)
521518
.catch(error => {
@@ -761,7 +758,7 @@ class DatabaseController {
761758
if (acl) {
762759
query = addWriteACL(query, acl);
763760
}
764-
validateQuery(query);
761+
validateQuery(query, isMaster, false);
765762
return schemaController
766763
.getOneSchema(className)
767764
.catch(error => {
@@ -1253,7 +1250,7 @@ class DatabaseController {
12531250
query = addReadACL(query, aclGroup);
12541251
}
12551252
}
1256-
validateQuery(query);
1253+
validateQuery(query, isMaster, false);
12571254
if (count) {
12581255
if (!classExists) {
12591256
return 0;
@@ -1809,7 +1806,7 @@ class DatabaseController {
18091806
return Promise.resolve(response);
18101807
}
18111808

1812-
static _validateQuery: any => void;
1809+
static _validateQuery: (any, boolean, boolean) => void;
18131810
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
18141811
}
18151812

src/RestQuery.js

+27
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) {
202202
.then(() => {
203203
return this.buildRestWhere();
204204
})
205+
.then(() => {
206+
return this.denyProtectedFields();
207+
})
205208
.then(() => {
206209
return this.handleIncludeAll();
207210
})
@@ -688,6 +691,30 @@ RestQuery.prototype.runCount = function () {
688691
});
689692
};
690693

694+
RestQuery.prototype.denyProtectedFields = async function () {
695+
if (this.auth.isMaster) {
696+
return;
697+
}
698+
const schemaController = await this.config.database.loadSchema();
699+
const protectedFields =
700+
this.config.database.addProtectedFields(
701+
schemaController,
702+
this.className,
703+
this.restWhere,
704+
this.findOptions.acl,
705+
this.auth,
706+
this.findOptions
707+
) || [];
708+
for (const key of protectedFields) {
709+
if (this.restWhere[key]) {
710+
throw new Parse.Error(
711+
Parse.Error.OPERATION_FORBIDDEN,
712+
`This user is not allowed to query ${key} on class ${this.className}`
713+
);
714+
}
715+
}
716+
};
717+
691718
// Augments this.response with all pointers on an object
692719
RestQuery.prototype.handleIncludeAll = function () {
693720
if (!this.includeAll) {

0 commit comments

Comments
 (0)