Skip to content

Commit eba2656

Browse files
authored
test: LiveQuery regex timeout and context isolation are not exploitable (#10226)
1 parent eecd953 commit eba2656

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

spec/QueryTools.spec.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)