@@ -3404,6 +3404,333 @@ describe('(GHSA-5hmj-jcgp-6hff) Protected fields leak via LiveQuery afterEvent t
34043404 ] ) ;
34053405 } ) ;
34063406
3407+ describe ( '(GHSA-m983-v2ff-wq65) LiveQuery shared mutable state race across concurrent subscribers' , ( ) => {
3408+ // Helper: create a LiveQuery client, wait for open, subscribe, wait for subscription ACK
3409+ async function createSubscribedClient ( { className, masterKey, installationId } ) {
3410+ const opts = {
3411+ applicationId : 'test' ,
3412+ serverURL : 'ws://localhost:8378' ,
3413+ javascriptKey : 'test' ,
3414+ } ;
3415+ if ( masterKey ) {
3416+ opts . masterKey = 'test' ;
3417+ }
3418+ if ( installationId ) {
3419+ opts . installationId = installationId ;
3420+ }
3421+ const client = new Parse . LiveQueryClient ( opts ) ;
3422+ client . open ( ) ;
3423+ const query = new Parse . Query ( className ) ;
3424+ const sub = client . subscribe ( query ) ;
3425+ await new Promise ( resolve => sub . on ( 'open' , resolve ) ) ;
3426+ return { client, sub } ;
3427+ }
3428+
3429+ async function setupProtectedClass ( className ) {
3430+ const config = Config . get ( Parse . applicationId ) ;
3431+ const schemaController = await config . database . loadSchema ( ) ;
3432+ await schemaController . addClassIfNotExists ( className , {
3433+ secretField : { type : 'String' } ,
3434+ publicField : { type : 'String' } ,
3435+ } ) ;
3436+ await schemaController . updateClass (
3437+ className ,
3438+ { } ,
3439+ {
3440+ find : { '*' : true } ,
3441+ get : { '*' : true } ,
3442+ create : { '*' : true } ,
3443+ update : { '*' : true } ,
3444+ delete : { '*' : true } ,
3445+ addField : { } ,
3446+ protectedFields : { '*' : [ 'secretField' ] } ,
3447+ }
3448+ ) ;
3449+ }
3450+
3451+ it ( 'should deliver protected fields to master key LiveQuery client' , async ( ) => {
3452+ const className = 'MasterKeyProtectedClass' ;
3453+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3454+ await reconfigureServer ( {
3455+ liveQuery : { classNames : [ className ] } ,
3456+ liveQueryServerOptions : {
3457+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3458+ } ,
3459+ verbose : false ,
3460+ silent : true ,
3461+ } ) ;
3462+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3463+ await setupProtectedClass ( className ) ;
3464+
3465+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3466+ className,
3467+ masterKey : true ,
3468+ } ) ;
3469+
3470+ try {
3471+ const result = new Promise ( resolve => {
3472+ masterSub . on ( 'create' , object => {
3473+ resolve ( {
3474+ secretField : object . get ( 'secretField' ) ,
3475+ publicField : object . get ( 'publicField' ) ,
3476+ } ) ;
3477+ } ) ;
3478+ } ) ;
3479+
3480+ const obj = new Parse . Object ( className ) ;
3481+ obj . set ( 'secretField' , 'MASTER_VISIBLE' ) ;
3482+ obj . set ( 'publicField' , 'public' ) ;
3483+ await obj . save ( null , { useMasterKey : true } ) ;
3484+
3485+ const received = await result ;
3486+
3487+ // Master key client must see protected fields
3488+ expect ( received . secretField ) . toBe ( 'MASTER_VISIBLE' ) ;
3489+ expect ( received . publicField ) . toBe ( 'public' ) ;
3490+ } finally {
3491+ masterClient . close ( ) ;
3492+ }
3493+ } ) ;
3494+
3495+ it ( 'should not leak protected fields to regular client when master key client subscribes concurrently on update' , async ( ) => {
3496+ const className = 'RaceUpdateClass' ;
3497+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3498+ await reconfigureServer ( {
3499+ liveQuery : { classNames : [ className ] } ,
3500+ liveQueryServerOptions : {
3501+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3502+ } ,
3503+ verbose : false ,
3504+ silent : true ,
3505+ } ) ;
3506+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3507+ await setupProtectedClass ( className ) ;
3508+
3509+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3510+ className,
3511+ masterKey : true ,
3512+ } ) ;
3513+ const { client : regularClient , sub : regularSub } = await createSubscribedClient ( {
3514+ className,
3515+ masterKey : false ,
3516+ } ) ;
3517+
3518+ try {
3519+ const obj = new Parse . Object ( className ) ;
3520+ obj . set ( 'secretField' , 'TOP_SECRET' ) ;
3521+ obj . set ( 'publicField' , 'visible' ) ;
3522+ await obj . save ( null , { useMasterKey : true } ) ;
3523+
3524+ const masterResult = new Promise ( resolve => {
3525+ masterSub . on ( 'update' , object => {
3526+ resolve ( {
3527+ secretField : object . get ( 'secretField' ) ,
3528+ publicField : object . get ( 'publicField' ) ,
3529+ } ) ;
3530+ } ) ;
3531+ } ) ;
3532+ const regularResult = new Promise ( resolve => {
3533+ regularSub . on ( 'update' , object => {
3534+ resolve ( {
3535+ secretField : object . get ( 'secretField' ) ,
3536+ publicField : object . get ( 'publicField' ) ,
3537+ } ) ;
3538+ } ) ;
3539+ } ) ;
3540+
3541+ await obj . save ( { publicField : 'updated' } , { useMasterKey : true } ) ;
3542+ const [ master , regular ] = await Promise . all ( [ masterResult , regularResult ] ) ;
3543+ // Regular client must NOT see the secret field
3544+ expect ( regular . secretField ) . toBeUndefined ( ) ;
3545+ expect ( regular . publicField ) . toBe ( 'updated' ) ;
3546+ // Master client must see the secret field
3547+ expect ( master . secretField ) . toBe ( 'TOP_SECRET' ) ;
3548+ expect ( master . publicField ) . toBe ( 'updated' ) ;
3549+ } finally {
3550+ masterClient . close ( ) ;
3551+ regularClient . close ( ) ;
3552+ }
3553+ } ) ;
3554+
3555+ it ( 'should not leak protected fields to regular client when master key client subscribes concurrently on create' , async ( ) => {
3556+ const className = 'RaceCreateClass' ;
3557+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3558+ await reconfigureServer ( {
3559+ liveQuery : { classNames : [ className ] } ,
3560+ liveQueryServerOptions : {
3561+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3562+ } ,
3563+ verbose : false ,
3564+ silent : true ,
3565+ } ) ;
3566+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3567+ await setupProtectedClass ( className ) ;
3568+
3569+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3570+ className,
3571+ masterKey : true ,
3572+ } ) ;
3573+ const { client : regularClient , sub : regularSub } = await createSubscribedClient ( {
3574+ className,
3575+ masterKey : false ,
3576+ } ) ;
3577+
3578+ try {
3579+ const masterResult = new Promise ( resolve => {
3580+ masterSub . on ( 'create' , object => {
3581+ resolve ( {
3582+ secretField : object . get ( 'secretField' ) ,
3583+ publicField : object . get ( 'publicField' ) ,
3584+ } ) ;
3585+ } ) ;
3586+ } ) ;
3587+ const regularResult = new Promise ( resolve => {
3588+ regularSub . on ( 'create' , object => {
3589+ resolve ( {
3590+ secretField : object . get ( 'secretField' ) ,
3591+ publicField : object . get ( 'publicField' ) ,
3592+ } ) ;
3593+ } ) ;
3594+ } ) ;
3595+
3596+ const newObj = new Parse . Object ( className ) ;
3597+ newObj . set ( 'secretField' , 'SECRET' ) ;
3598+ newObj . set ( 'publicField' , 'public' ) ;
3599+ await newObj . save ( null , { useMasterKey : true } ) ;
3600+
3601+ const [ master , regular ] = await Promise . all ( [ masterResult , regularResult ] ) ;
3602+
3603+ expect ( regular . secretField ) . toBeUndefined ( ) ;
3604+ expect ( regular . publicField ) . toBe ( 'public' ) ;
3605+ expect ( master . secretField ) . toBe ( 'SECRET' ) ;
3606+ expect ( master . publicField ) . toBe ( 'public' ) ;
3607+ } finally {
3608+ masterClient . close ( ) ;
3609+ regularClient . close ( ) ;
3610+ }
3611+ } ) ;
3612+
3613+ it ( 'should not leak protected fields to regular client when master key client subscribes concurrently on delete' , async ( ) => {
3614+ const className = 'RaceDeleteClass' ;
3615+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3616+ await reconfigureServer ( {
3617+ liveQuery : { classNames : [ className ] } ,
3618+ liveQueryServerOptions : {
3619+ keyPairs : { masterKey : 'test' , javascriptKey : 'test' } ,
3620+ } ,
3621+ verbose : false ,
3622+ silent : true ,
3623+ } ) ;
3624+ Parse . Cloud . afterLiveQueryEvent ( className , ( ) => { } ) ;
3625+ await setupProtectedClass ( className ) ;
3626+
3627+ const { client : masterClient , sub : masterSub } = await createSubscribedClient ( {
3628+ className,
3629+ masterKey : true ,
3630+ } ) ;
3631+ const { client : regularClient , sub : regularSub } = await createSubscribedClient ( {
3632+ className,
3633+ masterKey : false ,
3634+ } ) ;
3635+
3636+ try {
3637+ const obj = new Parse . Object ( className ) ;
3638+ obj . set ( 'secretField' , 'SECRET' ) ;
3639+ obj . set ( 'publicField' , 'public' ) ;
3640+ await obj . save ( null , { useMasterKey : true } ) ;
3641+
3642+ const masterResult = new Promise ( resolve => {
3643+ masterSub . on ( 'delete' , object => {
3644+ resolve ( {
3645+ secretField : object . get ( 'secretField' ) ,
3646+ publicField : object . get ( 'publicField' ) ,
3647+ } ) ;
3648+ } ) ;
3649+ } ) ;
3650+ const regularResult = new Promise ( resolve => {
3651+ regularSub . on ( 'delete' , object => {
3652+ resolve ( {
3653+ secretField : object . get ( 'secretField' ) ,
3654+ publicField : object . get ( 'publicField' ) ,
3655+ } ) ;
3656+ } ) ;
3657+ } ) ;
3658+
3659+ await obj . destroy ( { useMasterKey : true } ) ;
3660+ const [ master , regular ] = await Promise . all ( [ masterResult , regularResult ] ) ;
3661+
3662+ expect ( regular . secretField ) . toBeUndefined ( ) ;
3663+ expect ( regular . publicField ) . toBe ( 'public' ) ;
3664+ expect ( master . secretField ) . toBe ( 'SECRET' ) ;
3665+ expect ( master . publicField ) . toBe ( 'public' ) ;
3666+ } finally {
3667+ masterClient . close ( ) ;
3668+ regularClient . close ( ) ;
3669+ }
3670+ } ) ;
3671+
3672+ it ( 'should not corrupt object when afterEvent trigger modifies res.object for one client' , async ( ) => {
3673+ const className = 'TriggerRaceClass' ;
3674+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
3675+ await reconfigureServer ( {
3676+ liveQuery : { classNames : [ className ] } ,
3677+ startLiveQueryServer : true ,
3678+ verbose : false ,
3679+ silent : true ,
3680+ } ) ;
3681+ Parse . Cloud . afterLiveQueryEvent ( className , req => {
3682+ if ( req . object ) {
3683+ req . object . set ( 'injected' , `for-${ req . installationId } ` ) ;
3684+ }
3685+ } ) ;
3686+ const config = Config . get ( Parse . applicationId ) ;
3687+ const schemaController = await config . database . loadSchema ( ) ;
3688+ await schemaController . addClassIfNotExists ( className , {
3689+ data : { type : 'String' } ,
3690+ injected : { type : 'String' } ,
3691+ } ) ;
3692+
3693+ const { client : client1 , sub : sub1 } = await createSubscribedClient ( {
3694+ className,
3695+ masterKey : false ,
3696+ installationId : 'client-1' ,
3697+ } ) ;
3698+ const { client : client2 , sub : sub2 } = await createSubscribedClient ( {
3699+ className,
3700+ masterKey : false ,
3701+ installationId : 'client-2' ,
3702+ } ) ;
3703+
3704+ try {
3705+ const result1 = new Promise ( resolve => {
3706+ sub1 . on ( 'create' , object => {
3707+ resolve ( { data : object . get ( 'data' ) , injected : object . get ( 'injected' ) } ) ;
3708+ } ) ;
3709+ } ) ;
3710+ const result2 = new Promise ( resolve => {
3711+ sub2 . on ( 'create' , object => {
3712+ resolve ( { data : object . get ( 'data' ) , injected : object . get ( 'injected' ) } ) ;
3713+ } ) ;
3714+ } ) ;
3715+
3716+ const newObj = new Parse . Object ( className ) ;
3717+ newObj . set ( 'data' , 'value' ) ;
3718+ await newObj . save ( null , { useMasterKey : true } ) ;
3719+
3720+ const [ r1 , r2 ] = await Promise . all ( [ result1 , result2 ] ) ;
3721+
3722+ expect ( r1 . data ) . toBe ( 'value' ) ;
3723+ expect ( r2 . data ) . toBe ( 'value' ) ;
3724+ expect ( r1 . injected ) . toBe ( 'for-client-1' ) ;
3725+ expect ( r2 . injected ) . toBe ( 'for-client-2' ) ;
3726+ expect ( r1 . injected ) . not . toBe ( r2 . injected ) ;
3727+ } finally {
3728+ client1 . close ( ) ;
3729+ client2 . close ( ) ;
3730+ }
3731+ } ) ;
3732+ } ) ;
3733+
34073734 describe ( '(GHSA-pfj7-wv7c-22pr) AuthData subset validation bypass with allowExpiredAuthDataToken' , ( ) => {
34083735 let validatorSpy ;
34093736
0 commit comments