@@ -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