@@ -4828,3 +4828,123 @@ describe('(GHSA-wp76-gg32-8258) /verifyPassword leaks raw authData via missing a
48284828 } ) ;
48294829 } ) ;
48304830} ) ;
4831+
4832+ describe ( '(GHSA-mmg8-87c5-jrc2) LiveQuery protected-field guard bypass via array-like $or/$and/$nor' , ( ) => {
4833+ const { sleep } = require ( '../lib/TestUtils' ) ;
4834+ let obj ;
4835+
4836+ beforeEach ( async ( ) => {
4837+ Parse . CoreManager . getLiveQueryController ( ) . setDefaultLiveQueryClient ( null ) ;
4838+ await reconfigureServer ( {
4839+ liveQuery : { classNames : [ 'SecretClass' ] } ,
4840+ startLiveQueryServer : true ,
4841+ verbose : false ,
4842+ silent : true ,
4843+ } ) ;
4844+ const config = Config . get ( Parse . applicationId ) ;
4845+ const schemaController = await config . database . loadSchema ( ) ;
4846+ await schemaController . addClassIfNotExists (
4847+ 'SecretClass' ,
4848+ { secretObj : { type : 'Object' } , publicField : { type : 'String' } } ,
4849+ ) ;
4850+ await schemaController . updateClass (
4851+ 'SecretClass' ,
4852+ { } ,
4853+ {
4854+ find : { '*' : true } ,
4855+ get : { '*' : true } ,
4856+ create : { '*' : true } ,
4857+ update : { '*' : true } ,
4858+ delete : { '*' : true } ,
4859+ addField : { } ,
4860+ protectedFields : { '*' : [ 'secretObj' ] } ,
4861+ }
4862+ ) ;
4863+
4864+ obj = new Parse . Object ( 'SecretClass' ) ;
4865+ obj . set ( 'secretObj' , { apiKey : 'SENSITIVE_KEY_123' , score : 42 } ) ;
4866+ obj . set ( 'publicField' , 'visible' ) ;
4867+ await obj . save ( null , { useMasterKey : true } ) ;
4868+ } ) ;
4869+
4870+ afterEach ( async ( ) => {
4871+ const client = await Parse . CoreManager . getLiveQueryController ( ) . getDefaultLiveQueryClient ( ) ;
4872+ if ( client ) {
4873+ await client . close ( ) ;
4874+ }
4875+ } ) ;
4876+
4877+ it ( 'should reject subscription with array-like $or containing protected field' , async ( ) => {
4878+ const query = new Parse . Query ( 'SecretClass' ) ;
4879+ query . _where = {
4880+ $or : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , length : 1 } ,
4881+ } ;
4882+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
4883+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
4884+ ) ;
4885+ } ) ;
4886+
4887+ it ( 'should reject subscription with array-like $and containing protected field' , async ( ) => {
4888+ const query = new Parse . Query ( 'SecretClass' ) ;
4889+ query . _where = {
4890+ $and : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , '1' : { publicField : 'visible' } , length : 2 } ,
4891+ } ;
4892+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
4893+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
4894+ ) ;
4895+ } ) ;
4896+
4897+ it ( 'should reject subscription with array-like $nor containing protected field' , async ( ) => {
4898+ const query = new Parse . Query ( 'SecretClass' ) ;
4899+ query . _where = {
4900+ $nor : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , length : 1 } ,
4901+ } ;
4902+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
4903+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
4904+ ) ;
4905+ } ) ;
4906+
4907+ it ( 'should reject subscription with array-like $or even on non-protected fields' , async ( ) => {
4908+ const query = new Parse . Query ( 'SecretClass' ) ;
4909+ query . _where = {
4910+ $or : { '0' : { publicField : 'visible' } , length : 1 } ,
4911+ } ;
4912+ await expectAsync ( query . subscribe ( ) ) . toBeRejectedWith (
4913+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
4914+ ) ;
4915+ } ) ;
4916+
4917+ it ( 'should not create oracle via array-like $or bypass on protected fields' , async ( ) => {
4918+ const query = new Parse . Query ( 'SecretClass' ) ;
4919+ query . _where = {
4920+ $or : { '0' : { 'secretObj.apiKey' : 'SENSITIVE_KEY_123' } , length : 1 } ,
4921+ } ;
4922+
4923+ // Subscription must be rejected; no event oracle should be possible
4924+ let subscriptionError ;
4925+ let subscription ;
4926+ try {
4927+ subscription = await query . subscribe ( ) ;
4928+ } catch ( e ) {
4929+ subscriptionError = e ;
4930+ }
4931+
4932+ if ( ! subscriptionError ) {
4933+ const updateSpy = jasmine . createSpy ( 'update' ) ;
4934+ subscription . on ( 'create' , updateSpy ) ;
4935+ subscription . on ( 'update' , updateSpy ) ;
4936+
4937+ // Trigger an object change
4938+ obj . set ( 'publicField' , 'changed' ) ;
4939+ await obj . save ( null , { useMasterKey : true } ) ;
4940+ await sleep ( 500 ) ;
4941+
4942+ // If subscription somehow accepted, verify no events fired (evaluator defense)
4943+ expect ( updateSpy ) . not . toHaveBeenCalled ( ) ;
4944+ fail ( 'Expected subscription to be rejected' ) ;
4945+ }
4946+ expect ( subscriptionError ) . toEqual (
4947+ jasmine . objectContaining ( { code : Parse . Error . INVALID_QUERY } )
4948+ ) ;
4949+ } ) ;
4950+ } ) ;
0 commit comments