Skip to content

Commit 976dad1

Browse files
authored
fix: LiveQuery bypasses CLP pointer permission enforcement ([GHSA-fph2-r4qg-9576](GHSA-fph2-r4qg-9576)) (#10252)
1 parent 5d9b5bd commit 976dad1

File tree

2 files changed

+372
-3
lines changed

2 files changed

+372
-3
lines changed

spec/vulnerabilities.spec.js

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3115,3 +3115,304 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
31153115
});
31163116
});
31173117
});
3118+
3119+
describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforcement', () => {
3120+
const { sleep } = require('../lib/TestUtils');
3121+
3122+
beforeEach(() => {
3123+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3124+
});
3125+
3126+
afterEach(async () => {
3127+
try {
3128+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
3129+
if (client) {
3130+
await client.close();
3131+
}
3132+
} catch (e) {
3133+
// Ignore cleanup errors when client is not initialized
3134+
}
3135+
});
3136+
3137+
async function updateCLP(className, permissions) {
3138+
const response = await fetch(Parse.serverURL + '/schemas/' + className, {
3139+
method: 'PUT',
3140+
headers: {
3141+
'X-Parse-Application-Id': Parse.applicationId,
3142+
'X-Parse-Master-Key': Parse.masterKey,
3143+
'Content-Type': 'application/json',
3144+
},
3145+
body: JSON.stringify({ classLevelPermissions: permissions }),
3146+
});
3147+
const body = await response.json();
3148+
if (body.error) {
3149+
throw body;
3150+
}
3151+
return body;
3152+
}
3153+
3154+
it('should not deliver LiveQuery events to user not in readUserFields pointer', async () => {
3155+
await reconfigureServer({
3156+
liveQuery: { classNames: ['PrivateMessage'] },
3157+
startLiveQueryServer: true,
3158+
verbose: false,
3159+
silent: true,
3160+
});
3161+
3162+
// Create users using master key to avoid session management issues
3163+
const userA = new Parse.User();
3164+
userA.setUsername('userA_pointer');
3165+
userA.setPassword('password123');
3166+
await userA.signUp();
3167+
await Parse.User.logOut();
3168+
3169+
// User B stays logged in for the subscription
3170+
const userB = new Parse.User();
3171+
userB.setUsername('userB_pointer');
3172+
userB.setPassword('password456');
3173+
await userB.signUp();
3174+
3175+
// Create schema by saving an object with owner pointer, then set CLP
3176+
const seed = new Parse.Object('PrivateMessage');
3177+
seed.set('owner', userA);
3178+
await seed.save(null, { useMasterKey: true });
3179+
await seed.destroy({ useMasterKey: true });
3180+
3181+
await updateCLP('PrivateMessage', {
3182+
create: { '*': true },
3183+
find: {},
3184+
get: {},
3185+
readUserFields: ['owner'],
3186+
});
3187+
3188+
// User B subscribes — should NOT receive events for User A's objects
3189+
const query = new Parse.Query('PrivateMessage');
3190+
const subscription = await query.subscribe(userB.getSessionToken());
3191+
3192+
const createSpy = jasmine.createSpy('create');
3193+
const enterSpy = jasmine.createSpy('enter');
3194+
subscription.on('create', createSpy);
3195+
subscription.on('enter', enterSpy);
3196+
3197+
// Create a message owned by User A
3198+
const msg = new Parse.Object('PrivateMessage');
3199+
msg.set('content', 'secret message');
3200+
msg.set('owner', userA);
3201+
await msg.save(null, { useMasterKey: true });
3202+
3203+
await sleep(500);
3204+
3205+
// User B should NOT have received the create event
3206+
expect(createSpy).not.toHaveBeenCalled();
3207+
expect(enterSpy).not.toHaveBeenCalled();
3208+
});
3209+
3210+
it('should deliver LiveQuery events to user in readUserFields pointer', async () => {
3211+
await reconfigureServer({
3212+
liveQuery: { classNames: ['PrivateMessage2'] },
3213+
startLiveQueryServer: true,
3214+
verbose: false,
3215+
silent: true,
3216+
});
3217+
3218+
// User A stays logged in for the subscription
3219+
const userA = new Parse.User();
3220+
userA.setUsername('userA_owner');
3221+
userA.setPassword('password123');
3222+
await userA.signUp();
3223+
3224+
// Create schema by saving an object with owner pointer
3225+
const seed = new Parse.Object('PrivateMessage2');
3226+
seed.set('owner', userA);
3227+
await seed.save(null, { useMasterKey: true });
3228+
await seed.destroy({ useMasterKey: true });
3229+
3230+
await updateCLP('PrivateMessage2', {
3231+
create: { '*': true },
3232+
find: {},
3233+
get: {},
3234+
readUserFields: ['owner'],
3235+
});
3236+
3237+
// User A subscribes — SHOULD receive events for their own objects
3238+
const query = new Parse.Query('PrivateMessage2');
3239+
const subscription = await query.subscribe(userA.getSessionToken());
3240+
3241+
const createSpy = jasmine.createSpy('create');
3242+
subscription.on('create', createSpy);
3243+
3244+
// Create a message owned by User A
3245+
const msg = new Parse.Object('PrivateMessage2');
3246+
msg.set('content', 'my own message');
3247+
msg.set('owner', userA);
3248+
await msg.save(null, { useMasterKey: true });
3249+
3250+
await sleep(500);
3251+
3252+
// User A SHOULD have received the create event
3253+
expect(createSpy).toHaveBeenCalledTimes(1);
3254+
});
3255+
3256+
it('should not deliver LiveQuery events when find uses pointerFields', async () => {
3257+
await reconfigureServer({
3258+
liveQuery: { classNames: ['PrivateDoc'] },
3259+
startLiveQueryServer: true,
3260+
verbose: false,
3261+
silent: true,
3262+
});
3263+
3264+
const userA = new Parse.User();
3265+
userA.setUsername('userA_doc');
3266+
userA.setPassword('password123');
3267+
await userA.signUp();
3268+
await Parse.User.logOut();
3269+
3270+
// User B stays logged in for the subscription
3271+
const userB = new Parse.User();
3272+
userB.setUsername('userB_doc');
3273+
userB.setPassword('password456');
3274+
await userB.signUp();
3275+
3276+
// Create schema by saving an object with recipient pointer
3277+
const seed = new Parse.Object('PrivateDoc');
3278+
seed.set('recipient', userA);
3279+
await seed.save(null, { useMasterKey: true });
3280+
await seed.destroy({ useMasterKey: true });
3281+
3282+
// Set CLP with pointerFields instead of readUserFields
3283+
await updateCLP('PrivateDoc', {
3284+
create: { '*': true },
3285+
find: { pointerFields: ['recipient'] },
3286+
get: { pointerFields: ['recipient'] },
3287+
});
3288+
3289+
// User B subscribes
3290+
const query = new Parse.Query('PrivateDoc');
3291+
const subscription = await query.subscribe(userB.getSessionToken());
3292+
3293+
const createSpy = jasmine.createSpy('create');
3294+
subscription.on('create', createSpy);
3295+
3296+
// Create doc with recipient = User A (not User B)
3297+
const doc = new Parse.Object('PrivateDoc');
3298+
doc.set('title', 'confidential');
3299+
doc.set('recipient', userA);
3300+
await doc.save(null, { useMasterKey: true });
3301+
3302+
await sleep(500);
3303+
3304+
// User B should NOT receive events for User A's document
3305+
expect(createSpy).not.toHaveBeenCalled();
3306+
});
3307+
3308+
it('should not deliver LiveQuery events to unauthenticated users for pointer-protected classes', async () => {
3309+
await reconfigureServer({
3310+
liveQuery: { classNames: ['SecureItem'] },
3311+
startLiveQueryServer: true,
3312+
verbose: false,
3313+
silent: true,
3314+
});
3315+
3316+
const userA = new Parse.User();
3317+
userA.setUsername('userA_secure');
3318+
userA.setPassword('password123');
3319+
await userA.signUp();
3320+
await Parse.User.logOut();
3321+
3322+
// Create schema
3323+
const seed = new Parse.Object('SecureItem');
3324+
seed.set('owner', userA);
3325+
await seed.save(null, { useMasterKey: true });
3326+
await seed.destroy({ useMasterKey: true });
3327+
3328+
await updateCLP('SecureItem', {
3329+
create: { '*': true },
3330+
find: {},
3331+
get: {},
3332+
readUserFields: ['owner'],
3333+
});
3334+
3335+
// Unauthenticated subscription
3336+
const query = new Parse.Query('SecureItem');
3337+
const subscription = await query.subscribe();
3338+
3339+
const createSpy = jasmine.createSpy('create');
3340+
subscription.on('create', createSpy);
3341+
3342+
const item = new Parse.Object('SecureItem');
3343+
item.set('data', 'private');
3344+
item.set('owner', userA);
3345+
await item.save(null, { useMasterKey: true });
3346+
3347+
await sleep(500);
3348+
3349+
expect(createSpy).not.toHaveBeenCalled();
3350+
});
3351+
3352+
it('should handle readUserFields with array of pointers', async () => {
3353+
await reconfigureServer({
3354+
liveQuery: { classNames: ['SharedDoc'] },
3355+
startLiveQueryServer: true,
3356+
verbose: false,
3357+
silent: true,
3358+
});
3359+
3360+
const userA = new Parse.User();
3361+
userA.setUsername('userA_shared');
3362+
userA.setPassword('password123');
3363+
await userA.signUp();
3364+
await Parse.User.logOut();
3365+
3366+
// User B — don't log out, session must remain valid
3367+
const userB = new Parse.User();
3368+
userB.setUsername('userB_shared');
3369+
userB.setPassword('password456');
3370+
await userB.signUp();
3371+
const userBSessionToken = userB.getSessionToken();
3372+
3373+
// User C — signUp changes current user to C, but B's session stays valid
3374+
const userC = new Parse.User();
3375+
userC.setUsername('userC_shared');
3376+
userC.setPassword('password789');
3377+
await userC.signUp();
3378+
const userCSessionToken = userC.getSessionToken();
3379+
3380+
// Create schema with array field
3381+
const seed = new Parse.Object('SharedDoc');
3382+
seed.set('collaborators', [userA]);
3383+
await seed.save(null, { useMasterKey: true });
3384+
await seed.destroy({ useMasterKey: true });
3385+
3386+
await updateCLP('SharedDoc', {
3387+
create: { '*': true },
3388+
find: {},
3389+
get: {},
3390+
readUserFields: ['collaborators'],
3391+
});
3392+
3393+
// User B subscribes — is in the collaborators array
3394+
const queryB = new Parse.Query('SharedDoc');
3395+
const subscriptionB = await queryB.subscribe(userBSessionToken);
3396+
const createSpyB = jasmine.createSpy('createB');
3397+
subscriptionB.on('create', createSpyB);
3398+
3399+
// User C subscribes — is NOT in the collaborators array
3400+
const queryC = new Parse.Query('SharedDoc');
3401+
const subscriptionC = await queryC.subscribe(userCSessionToken);
3402+
const createSpyC = jasmine.createSpy('createC');
3403+
subscriptionC.on('create', createSpyC);
3404+
3405+
// Create doc with collaborators = [userA, userB] (not userC)
3406+
const doc = new Parse.Object('SharedDoc');
3407+
doc.set('title', 'team doc');
3408+
doc.set('collaborators', [userA, userB]);
3409+
await doc.save(null, { useMasterKey: true });
3410+
3411+
await sleep(500);
3412+
3413+
// User B SHOULD receive the event (in collaborators array)
3414+
expect(createSpyB).toHaveBeenCalledTimes(1);
3415+
// User C should NOT receive the event
3416+
expect(createSpyC).not.toHaveBeenCalled();
3417+
});
3418+
});

0 commit comments

Comments
 (0)