@@ -3072,6 +3072,334 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
30723072 ] ) ;
30733073 } ) ;
30743074
3075+ describe ( '(GHSA-m983-v2ff-wq65) LiveQuery shared mutable state race across concurrent subscribers' , ( ) => {
3076+ // Helper: create a LiveQuery client, wait for open, subscribe, wait for subscription ACK
3077+ async function createSubscribedClient ( { className, masterKey, installationId } ) {
3078+ const opts = {
3079+ applicationId : 'test' ,
3080+ serverURL : 'ws://localhost:8378' ,
3081+ javascriptKey : 'test' ,
3082+ } ;
3083+ if ( masterKey ) {
3084+ opts . masterKey = 'test' ;
3085+ }
3086+ if ( installationId ) {
3087+ opts . installationId = installationId ;
3088+ }
3089+ const client = new Parse . LiveQueryClient ( opts ) ;
3090+ client . open ( ) ;
3091+ const query = new Parse . Query ( className ) ;
3092+ const sub = client . subscribe ( query ) ;
3093+ await new Promise ( resolve => sub . on ( 'open' , resolve ) ) ;
3094+ return { client, sub } ;
3095+ }
3096+
3097+ async function setupProtectedClass ( className ) {
3098+ const config = Config . get ( Parse . applicationId ) ;
3099+ const schemaController = await config . database . loadSchema ( ) ;
3100+ await schemaController . addClassIfNotExists ( className , {
3101+ secretField : { type : 'String' } ,
3102+ publicField : { type : 'String' } ,
3103+ } ) ;
3104+ await schemaController . updateClass (
3105+ className ,
3106+ { } ,
3107+ {
3108+ find : { '*' : true } ,
3109+ get : { '*' : true } ,
3110+ create : { '*' : true } ,
3111+ update : { '*' : true } ,
3112+ delete : { '*' : true } ,
3113+ addField : { } ,
3114+ protectedFields : { '*' : [ 'secretField' ] } ,
3115+ }
3116+ ) ;
3117+ }
3118+
3119+ it ( 'should deliver protected fields to master key LiveQuery client' , async ( ) => {
3120+ const className = 'MasterKeyProtectedClass' ;
3121+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3122+ await reconfigureServer ( {
3123+ liveQuery : { classNames : [ className ] } ,
3124+ liveQueryServerOptions : {
3125+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3126+ } ,
3127+ verbose : false ,
3128+ silent : true ,
3129+ } ) ;
3130+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3131+ await setupProtectedClass ( className ) ;
3132+
3133+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3134+ className,
3135+ masterKey : true ,
3136+ } ) ;
3137+
3138+ try {
3139+ const result = new Promise ( resolve => {
3140+ masterSub . on ( 'create' , object => {
3141+ resolve ( {
3142+ secretField : object . get ( 'secretField' ) ,
3143+ publicField : object . get ( 'publicField' ) ,
3144+ } ) ;
3145+ } ) ;
3146+ } ) ;
3147+
3148+ const obj = new Parse . Object ( className ) ;
3149+ obj . set ( 'secretField' , 'MASTER_VISIBLE' ) ;
3150+ obj . set ( 'publicField' , 'public' ) ;
3151+ await obj . save ( null , { useMasterKey : true } ) ;
3152+
3153+ const received = await result ;
3154+
3155+ // Master key client must see protected fields
3156+ expect ( received . secretField ) . toBe ( 'MASTER_VISIBLE' ) ;
3157+ expect ( received . publicField ) . toBe ( 'public' ) ;
3158+ } finally {
3159+ masterClient . close ( ) ;
3160+ }
3161+ } ) ;
3162+
3163+ it ( 'should not leak protected fields to regular client when master key client subscribes concurrently on update' , async ( ) => {
3164+ const className = 'RaceUpdateClass' ;
3165+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3166+ await reconfigureServer ( {
3167+ liveQuery : { classNames : [ className ] } ,
3168+ liveQueryServerOptions : {
3169+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3170+ } ,
3171+ verbose : false ,
3172+ silent : true ,
3173+ } ) ;
3174+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3175+ await setupProtectedClass ( className ) ;
3176+
3177+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3178+ className,
3179+ masterKey : true ,
3180+ } ) ;
3181+ const { client : regularClient , sub : regularSub } = await createSubscribedClient ( {
3182+ className,
3183+ masterKey : false ,
3184+ } ) ;
3185+
3186+ try {
3187+ const obj = new Parse . Object ( className ) ;
3188+ obj . set ( 'secretField' , 'TOP_SECRET' ) ;
3189+ obj . set ( 'publicField' , 'visible' ) ;
3190+ await obj . save ( null , { useMasterKey : true } ) ;
3191+
3192+ const masterResult = new Promise ( resolve => {
3193+ masterSub . on ( 'update' , object => {
3194+ resolve ( {
3195+ secretField : object . get ( 'secretField' ) ,
3196+ publicField : object . get ( 'publicField' ) ,
3197+ } ) ;
3198+ } ) ;
3199+ } ) ;
3200+ const regularResult = new Promise ( resolve => {
3201+ regularSub . on ( 'update' , object => {
3202+ resolve ( {
3203+ secretField : object . get ( 'secretField' ) ,
3204+ publicField : object . get ( 'publicField' ) ,
3205+ } ) ;
3206+ } ) ;
3207+ } ) ;
3208+
3209+ await obj . save ( { publicField : 'updated' } , { useMasterKey : true } ) ;
3210+ const [ master , regular ] = await Promise . all ( [ masterResult , regularResult ] ) ;
3211+
3212+ // Regular client must NOT see the secret field
3213+ expect ( regular . secretField ) . toBeUndefined ( ) ;
3214+ expect ( regular . publicField ) . toBe ( 'updated' ) ;
3215+ // Master client must see the secret field
3216+ expect ( master . secretField ) . toBe ( 'TOP_SECRET' ) ;
3217+ expect ( master . publicField ) . toBe ( 'updated' ) ;
3218+ } finally {
3219+ masterClient . close ( ) ;
3220+ regularClient . close ( ) ;
3221+ }
3222+ } ) ;
3223+
3224+ it ( 'should not leak protected fields to regular client when master key client subscribes concurrently on create' , async ( ) => {
3225+ const className = 'RaceCreateClass' ;
3226+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3227+ await reconfigureServer ( {
3228+ liveQuery : { classNames : [ className ] } ,
3229+ liveQueryServerOptions : {
3230+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3231+ } ,
3232+ verbose : false ,
3233+ silent : true ,
3234+ } ) ;
3235+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3236+ await setupProtectedClass ( className ) ;
3237+
3238+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3239+ className,
3240+ masterKey : true ,
3241+ } ) ;
3242+ const { client : regularClient , sub : regularSub } = await createSubscribedClient ( {
3243+ className,
3244+ masterKey : false ,
3245+ } ) ;
3246+
3247+ try {
3248+ const masterResult = new Promise ( resolve => {
3249+ masterSub . on ( 'create' , object => {
3250+ resolve ( {
3251+ secretField : object . get ( 'secretField' ) ,
3252+ publicField : object . get ( 'publicField' ) ,
3253+ } ) ;
3254+ } ) ;
3255+ } ) ;
3256+ const regularResult = new Promise ( resolve => {
3257+ regularSub . on ( 'create' , object => {
3258+ resolve ( {
3259+ secretField : object . get ( 'secretField' ) ,
3260+ publicField : object . get ( 'publicField' ) ,
3261+ } ) ;
3262+ } ) ;
3263+ } ) ;
3264+
3265+ const newObj = new Parse . Object ( className ) ;
3266+ newObj . set ( 'secretField' , 'SECRET' ) ;
3267+ newObj . set ( 'publicField' , 'public' ) ;
3268+ await newObj . save ( null , { useMasterKey : true } ) ;
3269+
3270+ const [ master , regular ] = await Promise . all ( [ masterResult , regularResult ] ) ;
3271+
3272+ expect ( regular . secretField ) . toBeUndefined ( ) ;
3273+ expect ( regular . publicField ) . toBe ( 'public' ) ;
3274+ expect ( master . secretField ) . toBe ( 'SECRET' ) ;
3275+ expect ( master . publicField ) . toBe ( 'public' ) ;
3276+ } finally {
3277+ masterClient . close ( ) ;
3278+ regularClient . close ( ) ;
3279+ }
3280+ } ) ;
3281+
3282+ it ( 'should not leak protected fields to regular client when master key client subscribes concurrently on delete' , async ( ) => {
3283+ const className = 'RaceDeleteClass' ;
3284+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3285+ await reconfigureServer ( {
3286+ liveQuery : { classNames : [ className ] } ,
3287+ liveQueryServerOptions : {
3288+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3289+ } ,
3290+ verbose : false ,
3291+ silent : true ,
3292+ } ) ;
3293+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3294+ await setupProtectedClass ( className ) ;
3295+
3296+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3297+ className,
3298+ masterKey : true ,
3299+ } ) ;
3300+ const { client : regularClient , sub : regularSub } = await createSubscribedClient ( {
3301+ className,
3302+ masterKey : false ,
3303+ } ) ;
3304+
3305+ try {
3306+ const obj = new Parse . Object ( className ) ;
3307+ obj . set ( 'secretField' , 'SECRET' ) ;
3308+ obj . set ( 'publicField' , 'public' ) ;
3309+ await obj . save ( null , { useMasterKey : true } ) ;
3310+
3311+ const masterResult = new Promise ( resolve => {
3312+ masterSub . on ( 'delete' , object => {
3313+ resolve ( {
3314+ secretField : object . get ( 'secretField' ) ,
3315+ publicField : object . get ( 'publicField' ) ,
3316+ } ) ;
3317+ } ) ;
3318+ } ) ;
3319+ const regularResult = new Promise ( resolve => {
3320+ regularSub . on ( 'delete' , object => {
3321+ resolve ( {
3322+ secretField : object . get ( 'secretField' ) ,
3323+ publicField : object . get ( 'publicField' ) ,
3324+ } ) ;
3325+ } ) ;
3326+ } ) ;
3327+
3328+ await obj . destroy ( { useMasterKey : true } ) ;
3329+ const [ master , regular ] = await Promise . all ( [ masterResult , regularResult ] ) ;
3330+
3331+ expect ( regular . secretField ) . toBeUndefined ( ) ;
3332+ expect ( regular . publicField ) . toBe ( 'public' ) ;
3333+ expect ( master . secretField ) . toBe ( 'SECRET' ) ;
3334+ expect ( master . publicField ) . toBe ( 'public' ) ;
3335+ } finally {
3336+ masterClient . close ( ) ;
3337+ regularClient . close ( ) ;
3338+ }
3339+ } ) ;
3340+
3341+ it ( 'should not corrupt object when afterEvent trigger modifies res.object for one client' , async ( ) => {
3342+ const className = 'TriggerRaceClass' ;
3343+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3344+ await reconfigureServer ( {
3345+ liveQuery : { classNames : [ className ] } ,
3346+ startLiveQueryServer : true ,
3347+ verbose : false ,
3348+ silent : true ,
3349+ } ) ;
3350+ Parse . Cloud . afterLiveQueryEvent ( className , req => {
3351+ if ( req . object ) {
3352+ req . object . set ( 'injected' , `for-${ req . installationId } ` ) ;
3353+ }
3354+ } ) ;
3355+ const config = Config . get ( Parse . applicationId ) ;
3356+ const schemaController = await config . database . loadSchema ( ) ;
3357+ await schemaController . addClassIfNotExists ( className , {
3358+ data : { type : 'String' } ,
3359+ injected : { type : 'String' } ,
3360+ } ) ;
3361+
3362+ const { client : client1 , sub : sub1 } = await createSubscribedClient ( {
3363+ className,
3364+ masterKey : false ,
3365+ installationId : 'client-1' ,
3366+ } ) ;
3367+ const { client : client2 , sub : sub2 } = await createSubscribedClient ( {
3368+ className,
3369+ masterKey : false ,
3370+ installationId : 'client-2' ,
3371+ } ) ;
3372+
3373+ try {
3374+ const result1 = new Promise ( resolve => {
3375+ sub1 . on ( 'create' , object => {
3376+ resolve ( { data : object . get ( 'data' ) , injected : object . get ( 'injected' ) } ) ;
3377+ } ) ;
3378+ } ) ;
3379+ const result2 = new Promise ( resolve => {
3380+ sub2 . on ( 'create' , object => {
3381+ resolve ( { data : object . get ( 'data' ) , injected : object . get ( 'injected' ) } ) ;
3382+ } ) ;
3383+ } ) ;
3384+
3385+ const newObj = new Parse . Object ( className ) ;
3386+ newObj . set ( 'data' , 'value' ) ;
3387+ await newObj . save ( null , { useMasterKey : true } ) ;
3388+
3389+ const [ r1 , r2 ] = await Promise . all ( [ result1 , result2 ] ) ;
3390+
3391+ expect ( r1 . data ) . toBe ( 'value' ) ;
3392+ expect ( r2 . data ) . toBe ( 'value' ) ;
3393+ expect ( r1 . injected ) . toBe ( 'for-client-1' ) ;
3394+ expect ( r2 . injected ) . toBe ( 'for-client-2' ) ;
3395+ expect ( r1 . injected ) . not . toBe ( r2 . injected ) ;
3396+ } finally {
3397+ client1 . close ( ) ;
3398+ client2 . close ( ) ;
3399+ }
3400+ } ) ;
3401+ } ) ;
3402+
30753403 describe ( '(GHSA-pfj7-wv7c-22pr) AuthData subset validation bypass with allowExpiredAuthDataToken' , ( ) => {
30763404 let validatorSpy ;
30773405
0 commit comments