Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions spec/RequestComplexity.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ describe('request complexity', () => {
await reconfigureServer({});
const config = Config.get('test');
expect(config.requestComplexity).toEqual({
allowRegex: true,
batchRequestLimit: -1,
includeDepth: -1,
includeCount: -1,
Expand Down Expand Up @@ -540,4 +541,174 @@ describe('request complexity', () => {
).toBeResolved();
});
});

describe('allowRegex', () => {
let config;

beforeEach(async () => {
await reconfigureServer({
requestComplexity: { allowRegex: false },
});
config = Config.get('test');
});

it('should reject $regex query when allowRegex is false (unauthenticated)', async () => {
const where = { username: { $regex: 'test' } };
await expectAsync(
rest.find(config, auth.nobody(config), '_User', where)
).toBeRejectedWith(
jasmine.objectContaining({
message: '$regex operator is not allowed',
})
);
});

it('should reject $regex query when allowRegex is false (authenticated user)', async () => {
const user = new Parse.User();
user.setUsername('testuser');
user.setPassword('testpass');
await user.signUp();
const userAuth = new auth.Auth({
config,
isMaster: false,
user,
});
const where = { username: { $regex: 'test' } };
await expectAsync(
rest.find(config, userAuth, '_User', where)
).toBeRejectedWith(
jasmine.objectContaining({
message: '$regex operator is not allowed',
})
);
});

it('should allow $regex query when allowRegex is false with master key', async () => {
const where = { username: { $regex: 'test' } };
await expectAsync(
rest.find(config, auth.master(config), '_User', where)
).toBeResolved();
});

it('should allow $regex query when allowRegex is true (default)', async () => {
await reconfigureServer({
requestComplexity: { allowRegex: true },
});
config = Config.get('test');
const where = { username: { $regex: 'test' } };
await expectAsync(
rest.find(config, auth.nobody(config), '_User', where)
).toBeResolved();
});

it('should reject $regex inside $or when allowRegex is false', async () => {
const where = {
$or: [
{ username: { $regex: 'test' } },
{ username: 'exact' },
],
};
await expectAsync(
rest.find(config, auth.nobody(config), '_User', where)
).toBeRejectedWith(
jasmine.objectContaining({
message: '$regex operator is not allowed',
})
);
});

it('should reject $regex inside $and when allowRegex is false', async () => {
const where = {
$and: [
{ username: { $regex: 'test' } },
{ username: 'exact' },
],
};
await expectAsync(
rest.find(config, auth.nobody(config), '_User', where)
).toBeRejectedWith(
jasmine.objectContaining({
message: '$regex operator is not allowed',
})
);
});

it('should reject $regex inside $nor when allowRegex is false', async () => {
const where = {
$nor: [
{ username: { $regex: 'test' } },
],
};
await expectAsync(
rest.find(config, auth.nobody(config), '_User', where)
).toBeRejectedWith(
jasmine.objectContaining({
message: '$regex operator is not allowed',
})
);
});

it('should allow $regex by default when allowRegex is not configured', async () => {
await reconfigureServer({});
config = Config.get('test');
const where = { username: { $regex: 'test' } };
await expectAsync(
rest.find(config, auth.nobody(config), '_User', where)
).toBeResolved();
});

it('should allow $regex with maintenance key when allowRegex is false', async () => {
const where = { username: { $regex: 'test' } };
await expectAsync(
rest.find(config, auth.maintenance(config), '_User', where)
).toBeResolved();
});

describe('LiveQuery', () => {
beforeEach(async () => {
await reconfigureServer({
requestComplexity: { allowRegex: false },
liveQuery: { classNames: ['TestObject'] },
startLiveQueryServer: true,
});
config = Config.get('test');
});

afterEach(async () => {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
if (client) {
await client.close();
}
});

it('should reject LiveQuery subscription with $regex when allowRegex is false', async () => {
const query = new Parse.Query('TestObject');
query.matches('field', /test/);
await expectAsync(query.subscribe()).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.INVALID_QUERY })
);
});

it('should reject LiveQuery subscription with $regex inside $or when allowRegex is false', async () => {
const query = new Parse.Query('TestObject');
query._where = {
$or: [
{ field: { $regex: 'test' } },
{ field: 'exact' },
],
};
await expectAsync(query.subscribe()).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.INVALID_QUERY })
);
});

it('should allow LiveQuery subscription without $regex when allowRegex is false', async () => {
const query = new Parse.Query('TestObject');
query.equalTo('field', 'test');
const subscription = await query.subscribe();
expect(subscription).toBeDefined();
subscription.unsubscribe();
});
});
});
});
7 changes: 6 additions & 1 deletion src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,12 @@ export class Config {
for (const key of validKeys) {
if (requestComplexity[key] !== undefined) {
const value = requestComplexity[key];
if (!Number.isInteger(value) || (value < 1 && value !== -1)) {
const def = RequestComplexityOptions[key];
if (typeof def.default === 'boolean') {
if (typeof value !== 'boolean') {
throw new Error(`requestComplexity.${key} must be a boolean.`);
}
} else if (!Number.isInteger(value) || (value < 1 && value !== -1)) {
throw new Error(`requestComplexity.${key} must be a positive integer or -1 to disable.`);
}
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ const validateQuery = (

Object.keys(query).forEach(key => {
if (query && query[key] && query[key].$regex) {
if (!isMaster && rc && rc.allowRegex === false) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, '$regex operator is not allowed');
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (typeof query[key].$regex !== 'string') {
throw new Parse.Error(Parse.Error.INVALID_QUERY, '$regex value must be a string');
}
Expand Down
26 changes: 26 additions & 0 deletions src/LiveQuery/ParseLiveQueryServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,32 @@ class ParseLiveQueryServer {
}
}

// Validate allowRegex
if (!client.hasMasterKey) {
const rc = appConfig.requestComplexity;
if (rc && rc.allowRegex === false) {
const checkRegex = (where: any) => {
if (typeof where !== 'object' || where === null) {
return;
}
for (const key of Object.keys(where)) {
const constraint = where[key];
if (typeof constraint === 'object' && constraint !== null && constraint.$regex !== undefined) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, '$regex operator is not allowed');
}
}
for (const op of ['$or', '$and', '$nor']) {
if (Array.isArray(where[op])) {
for (const subQuery of where[op]) {
checkRegex(subQuery);
}
}
}
};
checkRegex(request.query.where);
}
}

// Check CLP for subscribe operation
const schemaController = await appConfig.database.loadSchema();
const classLevelPermissions = schemaController.getClassLevelPermissions(className);
Expand Down
6 changes: 6 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,12 @@ module.exports.RateLimitOptions = {
},
};
module.exports.RequestComplexityOptions = {
allowRegex: {
env: 'PARSE_SERVER_REQUEST_COMPLEXITY_ALLOW_REGEX',
help: 'Whether to allow the `$regex` query operator. Set to `false` to reject `$regex` in queries for non-master-key users. Default is `true`.',
action: parsers.booleanParser,
default: true,
},
batchRequestLimit: {
env: 'PARSE_SERVER_REQUEST_COMPLEXITY_BATCH_REQUEST_LIMIT',
help: 'Maximum number of sub-requests in a single batch request. Set to `-1` to disable. Default is `-1`.',
Expand Down
1 change: 1 addition & 0 deletions src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ export interface RateLimitOptions {
}

export interface RequestComplexityOptions {
/* Whether to allow the `$regex` query operator. Set to `false` to reject `$regex` in queries for non-master-key users. Default is `true`.
:ENV: PARSE_SERVER_REQUEST_COMPLEXITY_ALLOW_REGEX
:DEFAULT: true */
allowRegex: ?boolean;
/* Maximum depth of include pointer chains (e.g. `a.b.c` = depth 3). Set to `-1` to disable. Default is `-1`.
:DEFAULT: -1 */
includeDepth: ?number;
Expand Down
Loading