@@ -998,4 +998,118 @@ describe('matchesQuery', function () {
998998 expect ( matchesQuery ( obj2 , q ) ) . toBe ( true ) ;
999999 expect ( matchesQuery ( obj3 , q ) ) . toBe ( false ) ;
10001000 } ) ;
1001+
1002+ it ( 'terminates catastrophic backtracking regex within regexTimeout (GHSA-qxh4-6wmx-rhg9)' , function ( ) {
1003+ const { setRegexTimeout } = require ( '../lib/LiveQuery/QueryTools' ) ;
1004+ setRegexTimeout ( 100 ) ;
1005+ try {
1006+ const object = {
1007+ id : new Id ( 'Post' , 'P1' ) ,
1008+ title : 'aaaaaaaaaaaaaaaaaaaaaaaaaab' ,
1009+ } ;
1010+
1011+ // (a+)+$ is a classic catastrophic backtracking pattern
1012+ const q = new Parse . Query ( 'Post' ) ;
1013+ q . _where = { title : { $regex : '(a+)+$' } } ;
1014+
1015+ const start = Date . now ( ) ;
1016+ // With timeout protection, the regex should be terminated and return false
1017+ const result = matchesQuery ( object , q ) ;
1018+ const elapsed = Date . now ( ) - start ;
1019+
1020+ expect ( result ) . toBe ( false ) ;
1021+ // Should complete within a reasonable time (timeout + overhead), not hang
1022+ expect ( elapsed ) . toBeLessThan ( 5000 ) ;
1023+ } finally {
1024+ setRegexTimeout ( 0 ) ;
1025+ }
1026+ } ) ;
1027+
1028+ it ( 'applies default regexTimeout of 100ms protecting against ReDoS (GHSA-qxh4-6wmx-rhg9)' , async ( ) => {
1029+ await reconfigureServer ( {
1030+ liveQuery : { classNames : [ 'Post' ] } ,
1031+ } ) ;
1032+ const Config = require ( '../lib/Config' ) ;
1033+ const config = Config . get ( 'test' ) ;
1034+ // Default regexTimeout is 100ms, providing ReDoS protection out-of-the-box
1035+ expect ( config . liveQuery . regexTimeout ) . toBe ( 100 ) ;
1036+ expect ( config . liveQuery . regexTimeout ) . toBeGreaterThan ( 0 ) ;
1037+ } ) ;
1038+
1039+ it ( 'does not leak regex context between sequential evaluations with shared vmContext (GHSA-v88r-ghm9-267f)' , function ( ) {
1040+ const { setRegexTimeout } = require ( '../lib/LiveQuery/QueryTools' ) ;
1041+ setRegexTimeout ( 100 ) ;
1042+ try {
1043+ // Simulate the scenario from the advisory:
1044+ // Client A subscribes to { secretField: { $regex: "^admin" } }
1045+ // Client B subscribes to { publicField: { $regex: ".*" } }
1046+
1047+ // Object with a secretField that should only match Client A's subscription
1048+ const object = {
1049+ id : new Id ( 'Data' , 'D1' ) ,
1050+ secretField : 'admin_secret_data' ,
1051+ publicField : 'public_data' ,
1052+ } ;
1053+
1054+ // Client A's query: should match because secretField starts with "admin"
1055+ const queryA = new Parse . Query ( 'Data' ) ;
1056+ queryA . _where = { secretField : { $regex : '^admin' } } ;
1057+
1058+ // Client B's query: should match because publicField matches .*
1059+ const queryB = new Parse . Query ( 'Data' ) ;
1060+ queryB . _where = { publicField : { $regex : '.*' } } ;
1061+
1062+ // Evaluate both queries sequentially (as the LiveQuery server does)
1063+ const resultA = matchesQuery ( object , queryA ) ;
1064+ const resultB = matchesQuery ( object , queryB ) ;
1065+
1066+ // Both should match correctly — no cross-contamination
1067+ expect ( resultA ) . toBe ( true ) ;
1068+ expect ( resultB ) . toBe ( true ) ;
1069+
1070+ // Now test the inverse: object that should NOT match Client A
1071+ const object2 = {
1072+ id : new Id ( 'Data' , 'D2' ) ,
1073+ secretField : 'user_regular_data' ,
1074+ publicField : 'public_data' ,
1075+ } ;
1076+
1077+ const resultA2 = matchesQuery ( object2 , queryA ) ;
1078+ const resultB2 = matchesQuery ( object2 , queryB ) ;
1079+
1080+ // Client A should NOT match (secretField doesn't start with "admin")
1081+ // Client B should still match
1082+ expect ( resultA2 ) . toBe ( false ) ;
1083+ expect ( resultB2 ) . toBe ( true ) ;
1084+ } finally {
1085+ setRegexTimeout ( 0 ) ;
1086+ }
1087+ } ) ;
1088+
1089+ it ( 'does not cross-contaminate regex results across different field evaluations with regexTimeout (GHSA-v88r-ghm9-267f)' , function ( ) {
1090+ const { setRegexTimeout } = require ( '../lib/LiveQuery/QueryTools' ) ;
1091+ setRegexTimeout ( 100 ) ;
1092+ try {
1093+ // Multiple subscriptions with different regex patterns evaluated against
1094+ // different objects in rapid succession — the advisory claims the shared
1095+ // vmContext causes pattern/input from one call to leak into another
1096+ const subscriptions = [
1097+ { where : { field : { $regex : '^secret' } } , object : { id : new Id ( 'X' , '1' ) , field : 'secret_value' } , expected : true } ,
1098+ { where : { field : { $regex : '^public' } } , object : { id : new Id ( 'X' , '2' ) , field : 'public_value' } , expected : true } ,
1099+ { where : { field : { $regex : '^secret' } } , object : { id : new Id ( 'X' , '3' ) , field : 'public_value' } , expected : false } ,
1100+ { where : { field : { $regex : '^public' } } , object : { id : new Id ( 'X' , '4' ) , field : 'secret_value' } , expected : false } ,
1101+ { where : { field : { $regex : '^admin' } } , object : { id : new Id ( 'X' , '5' ) , field : 'admin_panel' } , expected : true } ,
1102+ { where : { field : { $regex : '^admin' } } , object : { id : new Id ( 'X' , '6' ) , field : 'user_panel' } , expected : false } ,
1103+ ] ;
1104+
1105+ for ( const sub of subscriptions ) {
1106+ const q = new Parse . Query ( 'X' ) ;
1107+ q . _where = sub . where ;
1108+ const result = matchesQuery ( sub . object , q ) ;
1109+ expect ( result ) . toBe ( sub . expected ) ;
1110+ }
1111+ } finally {
1112+ setRegexTimeout ( 0 ) ;
1113+ }
1114+ } ) ;
10011115} ) ;
0 commit comments