mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: LiveQuery subscription with invalid regular expression crashes server ([GHSA-827p-g5x5-h86c](https://github.com/parse-community/parse-server/security/advisories/GHSA-827p-g5x5-h86c)) (#10197)
This commit is contained in:
@@ -646,6 +646,82 @@ describe('ParseLiveQuery', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects subscription with invalid $regex pattern', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const query = new Parse.Query('TestObject');
|
||||
query._where = { foo: { $regex: '[invalid' } };
|
||||
await expectAsync(query.subscribe()).toBeRejectedWithError(/Invalid regular expression/);
|
||||
});
|
||||
|
||||
it('rejects subscription with non-string $regex value', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const query = new Parse.Query('TestObject');
|
||||
query._where = { foo: { $regex: 123 } };
|
||||
await expectAsync(query.subscribe()).toBeRejectedWithError(
|
||||
/\$regex must be a string or RegExp/
|
||||
);
|
||||
});
|
||||
|
||||
it('does not crash server when subscription matching throws and other subscriptions still work', async () => {
|
||||
const server = await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['TestObject'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const object = new TestObject();
|
||||
object.set('foo', 'bar');
|
||||
await object.save();
|
||||
|
||||
// Create a valid subscription
|
||||
const validQuery = new Parse.Query('TestObject');
|
||||
validQuery.equalTo('objectId', object.id);
|
||||
const validSubscription = await validQuery.subscribe();
|
||||
|
||||
// Inject a malformed subscription directly into the LiveQuery server
|
||||
// to bypass subscribe-time validation and test the try-catch in _onAfterSave
|
||||
const lqServer = server.liveQueryServer;
|
||||
const Subscription = require('../lib/LiveQuery/Subscription').Subscription;
|
||||
const badSubscription = new Subscription('TestObject', { foo: { $regex: '[invalid' } });
|
||||
badSubscription.addClientSubscription('fakeClientId', 'fakeRequestId');
|
||||
const classSubscriptions = lqServer.subscriptions.get('TestObject');
|
||||
classSubscriptions.set('bad-hash', badSubscription);
|
||||
|
||||
// Verify the valid subscription still receives updates despite the bad subscription
|
||||
const updatePromise = new Promise(resolve => {
|
||||
validSubscription.on('update', obj => {
|
||||
expect(obj.get('foo')).toBe('baz');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
object.set('foo', 'baz');
|
||||
await object.save();
|
||||
await updatePromise;
|
||||
|
||||
// Clean up the injected subscription
|
||||
classSubscriptions.delete('bad-hash');
|
||||
});
|
||||
|
||||
it('can handle mutate beforeSubscribe query', async done => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
|
||||
@@ -567,6 +567,49 @@ describe('matchesQuery', function () {
|
||||
expect(config.liveQuery.regexTimeout).toBe(100);
|
||||
});
|
||||
|
||||
it('does not throw on invalid $regex pattern', function () {
|
||||
const player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
};
|
||||
|
||||
// Invalid regex syntax should not throw, just return false
|
||||
const q = new Parse.Query('Player');
|
||||
q._where = { name: { $regex: '[invalid' } };
|
||||
expect(() => matchesQuery(player, q)).not.toThrow();
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not throw on invalid $regex pattern with regexTimeout enabled', function () {
|
||||
const { setRegexTimeout } = require('../lib/LiveQuery/QueryTools');
|
||||
setRegexTimeout(100);
|
||||
try {
|
||||
const player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
};
|
||||
|
||||
const q = new Parse.Query('Player');
|
||||
q._where = { name: { $regex: '[invalid' } };
|
||||
expect(() => matchesQuery(player, q)).not.toThrow();
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
} finally {
|
||||
setRegexTimeout(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not throw on invalid $regex flags', function () {
|
||||
const player = {
|
||||
id: new Id('Player', 'P1'),
|
||||
name: 'Player 1',
|
||||
};
|
||||
|
||||
const q = new Parse.Query('Player');
|
||||
q._where = { name: { $regex: 'valid', $options: 'xyz' } };
|
||||
expect(() => matchesQuery(player, q)).not.toThrow();
|
||||
expect(matchesQuery(player, q)).toBe(false);
|
||||
});
|
||||
|
||||
it('matches $nearSphere queries', function () {
|
||||
let q = new Parse.Query('Checkin');
|
||||
q.near('location', new Parse.GeoPoint(20, 20));
|
||||
|
||||
@@ -190,7 +190,13 @@ class ParseLiveQueryServer {
|
||||
}
|
||||
|
||||
for (const subscription of classSubscriptions.values()) {
|
||||
const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
||||
let isSubscriptionMatched;
|
||||
try {
|
||||
isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
||||
} catch (e) {
|
||||
logger.error(`Failed matching subscription for class ${className}: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
if (!isSubscriptionMatched) {
|
||||
continue;
|
||||
}
|
||||
@@ -286,14 +292,21 @@ class ParseLiveQueryServer {
|
||||
return;
|
||||
}
|
||||
for (const subscription of classSubscriptions.values()) {
|
||||
const isOriginalSubscriptionMatched = this._matchesSubscription(
|
||||
originalParseObject,
|
||||
subscription
|
||||
);
|
||||
const isCurrentSubscriptionMatched = this._matchesSubscription(
|
||||
currentParseObject,
|
||||
subscription
|
||||
);
|
||||
let isOriginalSubscriptionMatched;
|
||||
let isCurrentSubscriptionMatched;
|
||||
try {
|
||||
isOriginalSubscriptionMatched = this._matchesSubscription(
|
||||
originalParseObject,
|
||||
subscription
|
||||
);
|
||||
isCurrentSubscriptionMatched = this._matchesSubscription(
|
||||
currentParseObject,
|
||||
subscription
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(`Failed matching subscription for class ${className}: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
for (const [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
@@ -520,6 +533,53 @@ class ParseLiveQueryServer {
|
||||
});
|
||||
}
|
||||
|
||||
_validateQueryConstraints(where: any): void {
|
||||
if (typeof where !== 'object' || where === null) {
|
||||
return;
|
||||
}
|
||||
for (const key of Object.keys(where)) {
|
||||
const constraint = where[key];
|
||||
if (typeof constraint === 'object' && constraint !== null) {
|
||||
if (constraint.$regex !== undefined) {
|
||||
const regex = constraint.$regex;
|
||||
const isRegExpLike =
|
||||
regex !== null &&
|
||||
typeof regex === 'object' &&
|
||||
typeof regex.source === 'string' &&
|
||||
typeof regex.flags === 'string';
|
||||
if (typeof regex !== 'string' && !isRegExpLike) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Invalid regular expression: $regex must be a string or RegExp'
|
||||
);
|
||||
}
|
||||
const pattern = isRegExpLike ? regex.source : regex;
|
||||
const flags = isRegExpLike ? regex.flags : constraint.$options || '';
|
||||
try {
|
||||
new RegExp(pattern, flags);
|
||||
} catch (e) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Invalid regular expression: ${e.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const op of ['$or', '$and', '$nor']) {
|
||||
if (Array.isArray(constraint[op])) {
|
||||
constraint[op].forEach((subQuery: any) => {
|
||||
this._validateQueryConstraints(subQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (Array.isArray(where[key])) {
|
||||
where[key].forEach((subQuery: any) => {
|
||||
this._validateQueryConstraints(subQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_matchesSubscription(parseObject: any, subscription: any): boolean {
|
||||
// Object is undefined or null, not match
|
||||
if (!parseObject) {
|
||||
@@ -951,6 +1011,9 @@ class ParseLiveQueryServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate regex patterns in the subscription query
|
||||
this._validateQueryConstraints(request.query.where);
|
||||
|
||||
// Get subscription from subscriptions, create one if necessary
|
||||
const subscriptionHash = queryHash(request.query);
|
||||
// Add className to subscriptions if necessary
|
||||
|
||||
+17
-16
@@ -14,28 +14,29 @@ function setRegexTimeout(ms) {
|
||||
}
|
||||
|
||||
function safeRegexTest(pattern, flags, input) {
|
||||
if (!regexTimeout) {
|
||||
var re = new RegExp(pattern, flags);
|
||||
return re.test(input);
|
||||
}
|
||||
var cacheKey = flags + ':' + pattern;
|
||||
var script = scriptCache.get(cacheKey);
|
||||
if (!script) {
|
||||
if (scriptCache.size >= SCRIPT_CACHE_MAX) { scriptCache.clear(); }
|
||||
script = new vm.Script('new RegExp(pattern, flags).test(input)');
|
||||
scriptCache.set(cacheKey, script);
|
||||
}
|
||||
vmContext.pattern = pattern;
|
||||
vmContext.flags = flags;
|
||||
vmContext.input = input;
|
||||
try {
|
||||
if (!regexTimeout) {
|
||||
var re = new RegExp(pattern, flags);
|
||||
return re.test(input);
|
||||
}
|
||||
var cacheKey = flags + ':' + pattern;
|
||||
var script = scriptCache.get(cacheKey);
|
||||
if (!script) {
|
||||
if (scriptCache.size >= SCRIPT_CACHE_MAX) { scriptCache.clear(); }
|
||||
script = new vm.Script('new RegExp(pattern, flags).test(input)');
|
||||
scriptCache.set(cacheKey, script);
|
||||
}
|
||||
vmContext.pattern = pattern;
|
||||
vmContext.flags = flags;
|
||||
vmContext.input = input;
|
||||
return script.runInContext(vmContext, { timeout: regexTimeout });
|
||||
} catch (e) {
|
||||
if (e.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT') {
|
||||
logger.warn(`Regex timeout: pattern "${pattern}" with flags "${flags}" exceeded ${regexTimeout}ms limit`);
|
||||
return false;
|
||||
} else {
|
||||
logger.warn(`Invalid regex: pattern "${pattern}" with flags "${flags}": ${e.message}`);
|
||||
}
|
||||
throw e;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user