Skip to content

Commit 6c3317a

Browse files
authored
fix: LiveQuery bypasses CLP pointer permission enforcement ([GHSA-fph2-r4qg-9576](GHSA-fph2-r4qg-9576)) (#10250)
1 parent b50e14e commit 6c3317a

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
@@ -3349,3 +3349,304 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
33493349
});
33503350
});
33513351
});
3352+
3353+
describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforcement', () => {
3354+
const { sleep } = require('../lib/TestUtils');
3355+
3356+
beforeEach(() => {
3357+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3358+
});
3359+
3360+
afterEach(async () => {
3361+
try {
3362+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
3363+
if (client) {
3364+
await client.close();
3365+
}
3366+
} catch (e) {
3367+
// Ignore cleanup errors when client is not initialized
3368+
}
3369+
});
3370+
3371+
async function updateCLP(className, permissions) {
3372+
const response = await fetch(Parse.serverURL + '/schemas/' + className, {
3373+
method: 'PUT',
3374+
headers: {
3375+
'X-Parse-Application-Id': Parse.applicationId,
3376+
'X-Parse-Master-Key': Parse.masterKey,
3377+
'Content-Type': 'application/json',
3378+
},
3379+
body: JSON.stringify({ classLevelPermissions: permissions }),
3380+
});
3381+
const body = await response.json();
3382+
if (body.error) {
3383+
throw body;
3384+
}
3385+
return body;
3386+
}
3387+
3388+
it('should not deliver LiveQuery events to user not in readUserFields pointer', async () => {
3389+
await reconfigureServer({
3390+
liveQuery: { classNames: ['PrivateMessage'] },
3391+
startLiveQueryServer: true,
3392+
verbose: false,
3393+
silent: true,
3394+
});
3395+
3396+
// Create users using master key to avoid session management issues
3397+
const userA = new Parse.User();
3398+
userA.setUsername('userA_pointer');
3399+
userA.setPassword('password123');
3400+
await userA.signUp();
3401+
await Parse.User.logOut();
3402+
3403+
// User B stays logged in for the subscription
3404+
const userB = new Parse.User();
3405+
userB.setUsername('userB_pointer');
3406+
userB.setPassword('password456');
3407+
await userB.signUp();
3408+
3409+
// Create schema by saving an object with owner pointer, then set CLP
3410+
const seed = new Parse.Object('PrivateMessage');
3411+
seed.set('owner', userA);
3412+
await seed.save(null, { useMasterKey: true });
3413+
await seed.destroy({ useMasterKey: true });
3414+
3415+
await updateCLP('PrivateMessage', {
3416+
create: { '*': true },
3417+
find: {},
3418+
get: {},
3419+
readUserFields: ['owner'],
3420+
});
3421+
3422+
// User B subscribes — should NOT receive events for User A's objects
3423+
const query = new Parse.Query('PrivateMessage');
3424+
const subscription = await query.subscribe(userB.getSessionToken());
3425+
3426+
const createSpy = jasmine.createSpy('create');
3427+
const enterSpy = jasmine.createSpy('enter');
3428+
subscription.on('create', createSpy);
3429+
subscription.on('enter', enterSpy);
3430+
3431+
// Create a message owned by User A
3432+
const msg = new Parse.Object('PrivateMessage');
3433+
msg.set('content', 'secret message');
3434+
msg.set('owner', userA);
3435+
await msg.save(null, { useMasterKey: true });
3436+
3437+
await sleep(500);
3438+
3439+
// User B should NOT have received the create event
3440+
expect(createSpy).not.toHaveBeenCalled();
3441+
expect(enterSpy).not.toHaveBeenCalled();
3442+
});
3443+
3444+
it('should deliver LiveQuery events to user in readUserFields pointer', async () => {
3445+
await reconfigureServer({
3446+
liveQuery: { classNames: ['PrivateMessage2'] },
3447+
startLiveQueryServer: true,
3448+
verbose: false,
3449+
silent: true,
3450+
});
3451+
3452+
// User A stays logged in for the subscription
3453+
const userA = new Parse.User();
3454+
userA.setUsername('userA_owner');
3455+
userA.setPassword('password123');
3456+
await userA.signUp();
3457+
3458+
// Create schema by saving an object with owner pointer
3459+
const seed = new Parse.Object('PrivateMessage2');
3460+
seed.set('owner', userA);
3461+
await seed.save(null, { useMasterKey: true });
3462+
await seed.destroy({ useMasterKey: true });
3463+
3464+
await updateCLP('PrivateMessage2', {
3465+
create: { '*': true },
3466+
find: {},
3467+
get: {},
3468+
readUserFields: ['owner'],
3469+
});
3470+
3471+
// User A subscribes — SHOULD receive events for their own objects
3472+
const query = new Parse.Query('PrivateMessage2');
3473+
const subscription = await query.subscribe(userA.getSessionToken());
3474+
3475+
const createSpy = jasmine.createSpy('create');
3476+
subscription.on('create', createSpy);
3477+
3478+
// Create a message owned by User A
3479+
const msg = new Parse.Object('PrivateMessage2');
3480+
msg.set('content', 'my own message');
3481+
msg.set('owner', userA);
3482+
await msg.save(null, { useMasterKey: true });
3483+
3484+
await sleep(500);
3485+
3486+
// User A SHOULD have received the create event
3487+
expect(createSpy).toHaveBeenCalledTimes(1);
3488+
});
3489+
3490+
it('should not deliver LiveQuery events when find uses pointerFields', async () => {
3491+
await reconfigureServer({
3492+
liveQuery: { classNames: ['PrivateDoc'] },
3493+
startLiveQueryServer: true,
3494+
verbose: false,
3495+
silent: true,
3496+
});
3497+
3498+
const userA = new Parse.User();
3499+
userA.setUsername('userA_doc');
3500+
userA.setPassword('password123');
3501+
await userA.signUp();
3502+
await Parse.User.logOut();
3503+
3504+
// User B stays logged in for the subscription
3505+
const userB = new Parse.User();
3506+
userB.setUsername('userB_doc');
3507+
userB.setPassword('password456');
3508+
await userB.signUp();
3509+
3510+
// Create schema by saving an object with recipient pointer
3511+
const seed = new Parse.Object('PrivateDoc');
3512+
seed.set('recipient', userA);
3513+
await seed.save(null, { useMasterKey: true });
3514+
await seed.destroy({ useMasterKey: true });
3515+
3516+
// Set CLP with pointerFields instead of readUserFields
3517+
await updateCLP('PrivateDoc', {
3518+
create: { '*': true },
3519+
find: { pointerFields: ['recipient'] },
3520+
get: { pointerFields: ['recipient'] },
3521+
});
3522+
3523+
// User B subscribes
3524+
const query = new Parse.Query('PrivateDoc');
3525+
const subscription = await query.subscribe(userB.getSessionToken());
3526+
3527+
const createSpy = jasmine.createSpy('create');
3528+
subscription.on('create', createSpy);
3529+
3530+
// Create doc with recipient = User A (not User B)
3531+
const doc = new Parse.Object('PrivateDoc');
3532+
doc.set('title', 'confidential');
3533+
doc.set('recipient', userA);
3534+
await doc.save(null, { useMasterKey: true });
3535+
3536+
await sleep(500);
3537+
3538+
// User B should NOT receive events for User A's document
3539+
expect(createSpy).not.toHaveBeenCalled();
3540+
});
3541+
3542+
it('should not deliver LiveQuery events to unauthenticated users for pointer-protected classes', async () => {
3543+
await reconfigureServer({
3544+
liveQuery: { classNames: ['SecureItem'] },
3545+
startLiveQueryServer: true,
3546+
verbose: false,
3547+
silent: true,
3548+
});
3549+
3550+
const userA = new Parse.User();
3551+
userA.setUsername('userA_secure');
3552+
userA.setPassword('password123');
3553+
await userA.signUp();
3554+
await Parse.User.logOut();
3555+
3556+
// Create schema
3557+
const seed = new Parse.Object('SecureItem');
3558+
seed.set('owner', userA);
3559+
await seed.save(null, { useMasterKey: true });
3560+
await seed.destroy({ useMasterKey: true });
3561+
3562+
await updateCLP('SecureItem', {
3563+
create: { '*': true },
3564+
find: {},
3565+
get: {},
3566+
readUserFields: ['owner'],
3567+
});
3568+
3569+
// Unauthenticated subscription
3570+
const query = new Parse.Query('SecureItem');
3571+
const subscription = await query.subscribe();
3572+
3573+
const createSpy = jasmine.createSpy('create');
3574+
subscription.on('create', createSpy);
3575+
3576+
const item = new Parse.Object('SecureItem');
3577+
item.set('data', 'private');
3578+
item.set('owner', userA);
3579+
await item.save(null, { useMasterKey: true });
3580+
3581+
await sleep(500);
3582+
3583+
expect(createSpy).not.toHaveBeenCalled();
3584+
});
3585+
3586+
it('should handle readUserFields with array of pointers', async () => {
3587+
await reconfigureServer({
3588+
liveQuery: { classNames: ['SharedDoc'] },
3589+
startLiveQueryServer: true,
3590+
verbose: false,
3591+
silent: true,
3592+
});
3593+
3594+
const userA = new Parse.User();
3595+
userA.setUsername('userA_shared');
3596+
userA.setPassword('password123');
3597+
await userA.signUp();
3598+
await Parse.User.logOut();
3599+
3600+
// User B — don't log out, session must remain valid
3601+
const userB = new Parse.User();
3602+
userB.setUsername('userB_shared');
3603+
userB.setPassword('password456');
3604+
await userB.signUp();
3605+
const userBSessionToken = userB.getSessionToken();
3606+
3607+
// User C — signUp changes current user to C, but B's session stays valid
3608+
const userC = new Parse.User();
3609+
userC.setUsername('userC_shared');
3610+
userC.setPassword('password789');
3611+
await userC.signUp();
3612+
const userCSessionToken = userC.getSessionToken();
3613+
3614+
// Create schema with array field
3615+
const seed = new Parse.Object('SharedDoc');
3616+
seed.set('collaborators', [userA]);
3617+
await seed.save(null, { useMasterKey: true });
3618+
await seed.destroy({ useMasterKey: true });
3619+
3620+
await updateCLP('SharedDoc', {
3621+
create: { '*': true },
3622+
find: {},
3623+
get: {},
3624+
readUserFields: ['collaborators'],
3625+
});
3626+
3627+
// User B subscribes — is in the collaborators array
3628+
const queryB = new Parse.Query('SharedDoc');
3629+
const subscriptionB = await queryB.subscribe(userBSessionToken);
3630+
const createSpyB = jasmine.createSpy('createB');
3631+
subscriptionB.on('create', createSpyB);
3632+
3633+
// User C subscribes — is NOT in the collaborators array
3634+
const queryC = new Parse.Query('SharedDoc');
3635+
const subscriptionC = await queryC.subscribe(userCSessionToken);
3636+
const createSpyC = jasmine.createSpy('createC');
3637+
subscriptionC.on('create', createSpyC);
3638+
3639+
// Create doc with collaborators = [userA, userB] (not userC)
3640+
const doc = new Parse.Object('SharedDoc');
3641+
doc.set('title', 'team doc');
3642+
doc.set('collaborators', [userA, userB]);
3643+
await doc.save(null, { useMasterKey: true });
3644+
3645+
await sleep(500);
3646+
3647+
// User B SHOULD receive the event (in collaborators array)
3648+
expect(createSpyB).toHaveBeenCalledTimes(1);
3649+
// User C should NOT receive the event
3650+
expect(createSpyC).not.toHaveBeenCalled();
3651+
});
3652+
});

0 commit comments

Comments
 (0)