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