mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Protected fields leak via LiveQuery afterEvent trigger ([GHSA-5hmj-jcgp-6hff](https://github.com/parse-community/parse-server/security/advisories/GHSA-5hmj-jcgp-6hff)) (#10232)
This commit is contained in:
@@ -3092,3 +3092,133 @@ describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent trigger', () => {
|
||||
let obj;
|
||||
|
||||
beforeEach(async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['SecretClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
Parse.Cloud.afterLiveQueryEvent('SecretClass', () => {});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const schemaController = await config.database.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SecretClass', {
|
||||
secretField: { type: 'String' },
|
||||
publicField: { type: 'String' },
|
||||
});
|
||||
await schemaController.updateClass(
|
||||
'SecretClass',
|
||||
{},
|
||||
{
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: {},
|
||||
protectedFields: { '*': ['secretField'] },
|
||||
}
|
||||
);
|
||||
obj = new Parse.Object('SecretClass');
|
||||
obj.set('secretField', 'SENSITIVE_DATA');
|
||||
obj.set('publicField', 'visible');
|
||||
await obj.save(null, { useMasterKey: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not leak protected fields on update event when afterEvent trigger is registered', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
const subscription = await query.subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('update', (object, original) => {
|
||||
expect(object.get('secretField')).toBeUndefined();
|
||||
expect(object.get('publicField')).toBe('updated');
|
||||
expect(original.get('secretField')).toBeUndefined();
|
||||
expect(original.get('publicField')).toBe('visible');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
obj.save({ publicField: 'updated' }, { useMasterKey: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not leak protected fields on create event when afterEvent trigger is registered', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
const subscription = await query.subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('create', object => {
|
||||
expect(object.get('secretField')).toBeUndefined();
|
||||
expect(object.get('publicField')).toBe('new');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
new Parse.Object('SecretClass').save(
|
||||
{ secretField: 'SECRET', publicField: 'new' },
|
||||
{ useMasterKey: true }
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not leak protected fields on delete event when afterEvent trigger is registered', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
const subscription = await query.subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('delete', object => {
|
||||
expect(object.get('secretField')).toBeUndefined();
|
||||
expect(object.get('publicField')).toBe('visible');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
obj.destroy({ useMasterKey: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not leak protected fields on enter event when afterEvent trigger is registered', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.equalTo('publicField', 'match');
|
||||
const subscription = await query.subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('enter', (object, original) => {
|
||||
expect(object.get('secretField')).toBeUndefined();
|
||||
expect(object.get('publicField')).toBe('match');
|
||||
expect(original.get('secretField')).toBeUndefined();
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
obj.save({ publicField: 'match' }, { useMasterKey: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not leak protected fields on leave event when afterEvent trigger is registered', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.equalTo('publicField', 'visible');
|
||||
const subscription = await query.subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('leave', (object, original) => {
|
||||
expect(object.get('secretField')).toBeUndefined();
|
||||
expect(object.get('publicField')).toBe('changed');
|
||||
expect(original.get('secretField')).toBeUndefined();
|
||||
expect(original.get('publicField')).toBe('visible');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
obj.save({ publicField: 'changed' }, { useMasterKey: true }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -249,6 +249,7 @@ class ParseLiveQueryServer {
|
||||
if (res.object && typeof res.object.toJSON === 'function') {
|
||||
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
|
||||
}
|
||||
res.object = deletedParseObject;
|
||||
await this._filterSensitiveData(
|
||||
classLevelPermissions,
|
||||
res,
|
||||
@@ -257,6 +258,7 @@ class ParseLiveQueryServer {
|
||||
op,
|
||||
subscription.query
|
||||
);
|
||||
deletedParseObject = res.object;
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
} catch (e) {
|
||||
const error = resolveError(e);
|
||||
@@ -414,6 +416,8 @@ class ParseLiveQueryServer {
|
||||
res.original.className || className
|
||||
);
|
||||
}
|
||||
res.object = currentParseObject;
|
||||
res.original = originalParseObject;
|
||||
await this._filterSensitiveData(
|
||||
classLevelPermissions,
|
||||
res,
|
||||
@@ -422,6 +426,8 @@ class ParseLiveQueryServer {
|
||||
op,
|
||||
subscription.query
|
||||
);
|
||||
currentParseObject = res.object;
|
||||
originalParseObject = res.original ?? null;
|
||||
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
|
||||
if (client[functionName]) {
|
||||
client[functionName](requestId, currentParseObject, originalParseObject);
|
||||
|
||||
Reference in New Issue
Block a user