@@ -842,6 +842,126 @@ describe('Vulnerabilities', () => {
842842 await expectAsync ( obj . save ( ) ) . toBeResolved ( ) ;
843843 } ) ;
844844 } ) ;
845+
846+ describe ( '(GHSA-mmg8-87c5-jrc2) LiveQuery protected-field guard bypass via array-like $or/$and/$nor' , ( ) => {
847+ const { sleep } = require ( '../lib/TestUtils' ) ;
848+ let obj ;
849+
850+ beforeEach ( async ( ) => {
851+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
852+ await reconfigureServer ( {
853+ liveQuery : { classNames : [ 'SecretClass' ] } ,
854+ startLiveQueryServer : true ,
855+ verbose : false ,
856+ silent : true ,
857+ } ) ;
858+ const config = Config . get ( Parse . applicationId ) ;
859+ const schemaController = await config . database . loadSchema ( ) ;
860+ await schemaController . addClassIfNotExists (
861+ 'SecretClass' ,
862+ { secretObj : { type : 'Object' } , publicField : { type : 'String' } } ,
863+ ) ;
864+ await schemaController . updateClass (
865+ 'SecretClass' ,
866+ { } ,
867+ {
868+ find : { '*' : true } ,
869+ get : { '*' : true } ,
870+ create : { '*' : true } ,
871+ update : { '*' : true } ,
872+ delete : { '*' : true } ,
873+ addField : { } ,
874+ protectedFields : { '*' : [ 'secretObj' ] } ,
875+ }
876+ ) ;
877+
878+ obj = new Parse . Object ( 'SecretClass' ) ;
879+ obj . set ( 'secretObj' , { apiKey : 'SENSITIVE_KEY_123' , score : 42 } ) ;
880+ obj . set ( 'publicField' , 'visible' ) ;
881+ await obj . save ( null , { useMasterKey : true } ) ;
882+ } ) ;
883+
884+ afterEach ( async ( ) => {
885+ const client = await Parse . CoreManager . getLiveQueryController ( ) . getDefaultLiveQueryClient ( ) ;
886+ if ( client ) {
887+ await client . close ( ) ;
888+ }
889+ } ) ;
890+
891+ it ( 'should reject subscription with array-like $or containing protected field' , async ( ) => {
892+ const query = new Parse . Query ( 'SecretClass' ) ;
893+ query . _where = {
894+ $or : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , length : 1 } ,
895+ } ;
896+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
897+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
898+ ) ;
899+ } ) ;
900+
901+ it ( 'should reject subscription with array-like $and containing protected field' , async ( ) => {
902+ const query = new Parse . Query ( 'SecretClass' ) ;
903+ query . _where = {
904+ $and : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , '1' : { publicField : 'visible' } , length : 2 } ,
905+ } ;
906+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
907+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
908+ ) ;
909+ } ) ;
910+
911+ it ( 'should reject subscription with array-like $nor containing protected field' , async ( ) => {
912+ const query = new Parse . Query ( 'SecretClass' ) ;
913+ query . _where = {
914+ $nor : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , length : 1 } ,
915+ } ;
916+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
917+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
918+ ) ;
919+ } ) ;
920+
921+ it ( 'should reject subscription with array-like $or even on non-protected fields' , async ( ) => {
922+ const query = new Parse . Query ( 'SecretClass' ) ;
923+ query . _where = {
924+ $or : { '0' : { publicField : 'visible' } , length : 1 } ,
925+ } ;
926+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
927+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
928+ ) ;
929+ } ) ;
930+
931+ it ( 'should not create oracle via array-like $or bypass on protected fields' , async ( ) => {
932+ const query = new Parse . Query ( 'SecretClass' ) ;
933+ query . _where = {
934+ $or : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , length : 1 } ,
935+ } ;
936+
937+ // Subscription must be rejected; no event oracle should be possible
938+ let subscriptionError ;
939+ let subscription ;
940+ try {
941+ subscription = await query . subscribe ( ) ;
942+ } catch ( e ) {
943+ subscriptionError = e ;
944+ }
945+
946+ if ( ! subscriptionError ) {
947+ const updateSpy = jasmine . createSpy ( 'update' ) ;
948+ subscription . on ( 'create' , updateSpy ) ;
949+ subscription . on ( 'update' , updateSpy ) ;
950+
951+ // Trigger an object change
952+ obj . set ( 'publicField' , 'changed' ) ;
953+ await obj . save ( null , { useMasterKey : true } ) ;
954+ await sleep ( 500 ) ;
955+
956+ // If subscription somehow accepted, verify no events fired (evaluator defense)
957+ expect ( updateSpy ) . not . toHaveBeenCalled ( ) ;
958+ fail ( 'Expected subscription to be rejected' ) ;
959+ }
960+ expect ( subscriptionError ) . toEqual (
961+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
962+ ) ;
963+ } ) ;
964+ } ) ;
845965} ) ;
846966
847967describe ( '(GHSA-mf3j-86qx-cq5j) ReDoS via $regex in LiveQuery subscription' , ( ) => {
0 commit comments