Skip to content

Commit 6648500

Browse files
authored
fix: Protected fields leak via LiveQuery afterEvent trigger ([GHSA-5hmj-jcgp-6hff](GHSA-5hmj-jcgp-6hff)) (#10232)
1 parent 0a6c872 commit 6648500

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed

spec/vulnerabilities.spec.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3092,3 +3092,133 @@ describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => {
30923092
});
30933093
});
30943094
});
3095+
3096+
describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent trigger', () => {
3097+
let obj;
3098+
3099+
beforeEach(async () => {
3100+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3101+
await reconfigureServer({
3102+
liveQuery: { classNames: ['SecretClass'] },
3103+
startLiveQueryServer: true,
3104+
verbose: false,
3105+
silent: true,
3106+
});
3107+
Parse.Cloud.afterLiveQueryEvent('SecretClass', () => {});
3108+
const config = Config.get(Parse.applicationId);
3109+
const schemaController = await config.database.loadSchema();
3110+
await schemaController.addClassIfNotExists('SecretClass', {
3111+
secretField: { type: 'String' },
3112+
publicField: { type: 'String' },
3113+
});
3114+
await schemaController.updateClass(
3115+
'SecretClass',
3116+
{},
3117+
{
3118+
find: { '*': true },
3119+
get: { '*': true },
3120+
create: { '*': true },
3121+
update: { '*': true },
3122+
delete: { '*': true },
3123+
addField: {},
3124+
protectedFields: { '*': ['secretField'] },
3125+
}
3126+
);
3127+
obj = new Parse.Object('SecretClass');
3128+
obj.set('secretField', 'SENSITIVE_DATA');
3129+
obj.set('publicField', 'visible');
3130+
await obj.save(null, { useMasterKey: true });
3131+
});
3132+
3133+
afterEach(async () => {
3134+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
3135+
if (client) {
3136+
await client.close();
3137+
}
3138+
});
3139+
3140+
it('should not leak protected fields on update event when afterEvent trigger is registered', async () => {
3141+
const query = new Parse.Query('SecretClass');
3142+
const subscription = await query.subscribe();
3143+
await Promise.all([
3144+
new Promise(resolve => {
3145+
subscription.on('update', (object, original) => {
3146+
expect(object.get('secretField')).toBeUndefined();
3147+
expect(object.get('publicField')).toBe('updated');
3148+
expect(original.get('secretField')).toBeUndefined();
3149+
expect(original.get('publicField')).toBe('visible');
3150+
resolve();
3151+
});
3152+
}),
3153+
obj.save({ publicField: 'updated' }, { useMasterKey: true }),
3154+
]);
3155+
});
3156+
3157+
it('should not leak protected fields on create event when afterEvent trigger is registered', async () => {
3158+
const query = new Parse.Query('SecretClass');
3159+
const subscription = await query.subscribe();
3160+
await Promise.all([
3161+
new Promise(resolve => {
3162+
subscription.on('create', object => {
3163+
expect(object.get('secretField')).toBeUndefined();
3164+
expect(object.get('publicField')).toBe('new');
3165+
resolve();
3166+
});
3167+
}),
3168+
new Parse.Object('SecretClass').save(
3169+
{ secretField: 'SECRET', publicField: 'new' },
3170+
{ useMasterKey: true }
3171+
),
3172+
]);
3173+
});
3174+
3175+
it('should not leak protected fields on delete event when afterEvent trigger is registered', async () => {
3176+
const query = new Parse.Query('SecretClass');
3177+
const subscription = await query.subscribe();
3178+
await Promise.all([
3179+
new Promise(resolve => {
3180+
subscription.on('delete', object => {
3181+
expect(object.get('secretField')).toBeUndefined();
3182+
expect(object.get('publicField')).toBe('visible');
3183+
resolve();
3184+
});
3185+
}),
3186+
obj.destroy({ useMasterKey: true }),
3187+
]);
3188+
});
3189+
3190+
it('should not leak protected fields on enter event when afterEvent trigger is registered', async () => {
3191+
const query = new Parse.Query('SecretClass');
3192+
query.equalTo('publicField', 'match');
3193+
const subscription = await query.subscribe();
3194+
await Promise.all([
3195+
new Promise(resolve => {
3196+
subscription.on('enter', (object, original) => {
3197+
expect(object.get('secretField')).toBeUndefined();
3198+
expect(object.get('publicField')).toBe('match');
3199+
expect(original.get('secretField')).toBeUndefined();
3200+
resolve();
3201+
});
3202+
}),
3203+
obj.save({ publicField: 'match' }, { useMasterKey: true }),
3204+
]);
3205+
});
3206+
3207+
it('should not leak protected fields on leave event when afterEvent trigger is registered', async () => {
3208+
const query = new Parse.Query('SecretClass');
3209+
query.equalTo('publicField', 'visible');
3210+
const subscription = await query.subscribe();
3211+
await Promise.all([
3212+
new Promise(resolve => {
3213+
subscription.on('leave', (object, original) => {
3214+
expect(object.get('secretField')).toBeUndefined();
3215+
expect(object.get('publicField')).toBe('changed');
3216+
expect(original.get('secretField')).toBeUndefined();
3217+
expect(original.get('publicField')).toBe('visible');
3218+
resolve();
3219+
});
3220+
}),
3221+
obj.save({ publicField: 'changed' }, { useMasterKey: true }),
3222+
]);
3223+
});
3224+
});

src/LiveQuery/ParseLiveQueryServer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ class ParseLiveQueryServer {
249249
if (res.object && typeof res.object.toJSON === 'function') {
250250
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
251251
}
252+
res.object = deletedParseObject;
252253
await this._filterSensitiveData(
253254
classLevelPermissions,
254255
res,
@@ -257,6 +258,7 @@ class ParseLiveQueryServer {
257258
op,
258259
subscription.query
259260
);
261+
deletedParseObject = res.object;
260262
client.pushDelete(requestId, deletedParseObject);
261263
} catch (e) {
262264
const error = resolveError(e);
@@ -414,6 +416,8 @@ class ParseLiveQueryServer {
414416
res.original.className || className
415417
);
416418
}
419+
res.object = currentParseObject;
420+
res.original = originalParseObject;
417421
await this._filterSensitiveData(
418422
classLevelPermissions,
419423
res,
@@ -422,6 +426,8 @@ class ParseLiveQueryServer {
422426
op,
423427
subscription.query
424428
);
429+
currentParseObject = res.object;
430+
originalParseObject = res.original ?? null;
425431
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
426432
if (client[functionName]) {
427433
client[functionName](requestId, currentParseObject, originalParseObject);

0 commit comments

Comments
 (0)