fix: brute force guessing of user sensitive data via search patterns (GHSA-2m6g-crv8-p3c6) (#8146) [skip release]

This commit is contained in:
Manuel
2022-09-02 21:43:31 +02:00
committed by GitHub
parent 5432082d82
commit 4c0c7c77b7
3 changed files with 134 additions and 37 deletions

View File

@@ -191,6 +191,79 @@ describe('rest query', () => {
expect(result.results.length).toEqual(0); 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 => { it('query with wrongly encoded parameter', done => {
rest rest
.create(config, nobody, 'TestParameterEncode', { foo: 'bar' }) .create(config, nobody, 'TestParameterEncode', { foo: 'bar' })

View File

@@ -55,31 +55,27 @@ const transformObjectACL = ({ ACL, ...result }) => {
return result; return result;
}; };
const specialQuerykeys = [ const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm'];
'$and', const specialMasterQueryKeys = [
'$or', ...specialQueryKeys,
'$nor',
'_rperm',
'_wperm',
'_perishable_token',
'_email_verify_token', '_email_verify_token',
'_perishable_token',
'_tombstone',
'_email_verify_token_expires_at', '_email_verify_token_expires_at',
'_account_lockout_expires_at',
'_failed_login_count', '_failed_login_count',
'_account_lockout_expires_at',
'_password_changed_at',
'_password_history',
]; ];
const isSpecialQueryKey = key => { const validateQuery = (query: any, isMaster: boolean, update: boolean): void => {
return specialQuerykeys.indexOf(key) >= 0;
};
const validateQuery = (query: any): void => {
if (query.ACL) { if (query.ACL) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
} }
if (query.$or) { if (query.$or) {
if (query.$or instanceof Array) { if (query.$or instanceof Array) {
query.$or.forEach(validateQuery); query.$or.forEach(value => validateQuery(value, isMaster, update));
} else { } else {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
} }
@@ -87,7 +83,7 @@ const validateQuery = (query: any): void => {
if (query.$and) { if (query.$and) {
if (query.$and instanceof Array) { if (query.$and instanceof Array) {
query.$and.forEach(validateQuery); query.$and.forEach(value => validateQuery(value, isMaster, update));
} else { } else {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
} }
@@ -95,7 +91,7 @@ const validateQuery = (query: any): void => {
if (query.$nor) { if (query.$nor) {
if (query.$nor instanceof Array && query.$nor.length > 0) { if (query.$nor instanceof Array && query.$nor.length > 0) {
query.$nor.forEach(validateQuery); query.$nor.forEach(value => validateQuery(value, isMaster, update));
} else { } else {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INVALID_QUERY, 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}`); 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]); perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
} }
if (!isUserClass) { if (isUserClass) {
return object; 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) { if (isMaster) {
return object; return object;
} }
delete object._email_verify_token; for (const key in object) {
delete object._perishable_token; if (key.charAt(0) === '_') {
delete object._perishable_token_expires_at; delete object[key];
delete object._tombstone; }
delete object._email_verify_token_expires_at; }
delete object._failed_login_count;
delete object._account_lockout_expires_at; if (!isUserClass) {
delete object._password_changed_at; return object;
delete object._password_history; }
if (aclGroup.indexOf(object.objectId) > -1) { if (aclGroup.indexOf(object.objectId) > -1) {
return object; return object;
@@ -515,7 +512,7 @@ class DatabaseController {
if (acl) { if (acl) {
query = addWriteACL(query, acl); query = addWriteACL(query, acl);
} }
validateQuery(query); validateQuery(query, isMaster, true);
return schemaController return schemaController
.getOneSchema(className, true) .getOneSchema(className, true)
.catch(error => { .catch(error => {
@@ -761,7 +758,7 @@ class DatabaseController {
if (acl) { if (acl) {
query = addWriteACL(query, acl); query = addWriteACL(query, acl);
} }
validateQuery(query); validateQuery(query, isMaster, false);
return schemaController return schemaController
.getOneSchema(className) .getOneSchema(className)
.catch(error => { .catch(error => {
@@ -1253,7 +1250,7 @@ class DatabaseController {
query = addReadACL(query, aclGroup); query = addReadACL(query, aclGroup);
} }
} }
validateQuery(query); validateQuery(query, isMaster, false);
if (count) { if (count) {
if (!classExists) { if (!classExists) {
return 0; return 0;
@@ -1809,7 +1806,7 @@ class DatabaseController {
return Promise.resolve(response); 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; static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
} }

View File

@@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) {
.then(() => { .then(() => {
return this.buildRestWhere(); return this.buildRestWhere();
}) })
.then(() => {
return this.denyProtectedFields();
})
.then(() => { .then(() => {
return this.handleIncludeAll(); 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 // Augments this.response with all pointers on an object
RestQuery.prototype.handleIncludeAll = function () { RestQuery.prototype.handleIncludeAll = function () {
if (!this.includeAll) { if (!this.includeAll) {