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