Skip to content

Commit 2126fe4

Browse files
authored
fix: LiveQuery subscription query depth bypass ([GHSA-6qh5-m6g3-xhq6](GHSA-6qh5-m6g3-xhq6)) (#10259)
1 parent 6eebd6e commit 2126fe4

File tree

2 files changed

+144
-1
lines changed

2 files changed

+144
-1
lines changed

spec/vulnerabilities.spec.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3928,3 +3928,119 @@ describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via Live
39283928
});
39293929

39303930
});
3931+
3932+
describe('(GHSA-6qh5-m6g3-xhq6) LiveQuery query depth DoS via deeply nested subscription', () => {
3933+
afterEach(async () => {
3934+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
3935+
if (client) {
3936+
await client.close();
3937+
}
3938+
});
3939+
3940+
it('should reject LiveQuery subscription with deeply nested $or when queryDepth is set', async () => {
3941+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3942+
await reconfigureServer({
3943+
liveQuery: { classNames: ['TestClass'] },
3944+
startLiveQueryServer: true,
3945+
verbose: false,
3946+
silent: true,
3947+
requestComplexity: { queryDepth: 10 },
3948+
});
3949+
const query = new Parse.Query('TestClass');
3950+
let where = { field: 'value' };
3951+
for (let i = 0; i < 15; i++) {
3952+
where = { $or: [where] };
3953+
}
3954+
query._where = where;
3955+
await expectAsync(query.subscribe()).toBeRejectedWith(
3956+
jasmine.objectContaining({
3957+
code: Parse.Error.INVALID_QUERY,
3958+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
3959+
})
3960+
);
3961+
});
3962+
3963+
it('should reject LiveQuery subscription with deeply nested $and when queryDepth is set', async () => {
3964+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3965+
await reconfigureServer({
3966+
liveQuery: { classNames: ['TestClass'] },
3967+
startLiveQueryServer: true,
3968+
verbose: false,
3969+
silent: true,
3970+
requestComplexity: { queryDepth: 10 },
3971+
});
3972+
const query = new Parse.Query('TestClass');
3973+
let where = { field: 'value' };
3974+
for (let i = 0; i < 50; i++) {
3975+
where = { $and: [where] };
3976+
}
3977+
query._where = where;
3978+
await expectAsync(query.subscribe()).toBeRejectedWith(
3979+
jasmine.objectContaining({
3980+
code: Parse.Error.INVALID_QUERY,
3981+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
3982+
})
3983+
);
3984+
});
3985+
3986+
it('should reject LiveQuery subscription with deeply nested $nor when queryDepth is set', async () => {
3987+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3988+
await reconfigureServer({
3989+
liveQuery: { classNames: ['TestClass'] },
3990+
startLiveQueryServer: true,
3991+
verbose: false,
3992+
silent: true,
3993+
requestComplexity: { queryDepth: 10 },
3994+
});
3995+
const query = new Parse.Query('TestClass');
3996+
let where = { field: 'value' };
3997+
for (let i = 0; i < 50; i++) {
3998+
where = { $nor: [where] };
3999+
}
4000+
query._where = where;
4001+
await expectAsync(query.subscribe()).toBeRejectedWith(
4002+
jasmine.objectContaining({
4003+
code: Parse.Error.INVALID_QUERY,
4004+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
4005+
})
4006+
);
4007+
});
4008+
4009+
it('should allow LiveQuery subscription within the depth limit', async () => {
4010+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
4011+
await reconfigureServer({
4012+
liveQuery: { classNames: ['TestClass'] },
4013+
startLiveQueryServer: true,
4014+
verbose: false,
4015+
silent: true,
4016+
requestComplexity: { queryDepth: 10 },
4017+
});
4018+
const query = new Parse.Query('TestClass');
4019+
let where = { field: 'value' };
4020+
for (let i = 0; i < 5; i++) {
4021+
where = { $or: [where] };
4022+
}
4023+
query._where = where;
4024+
const subscription = await query.subscribe();
4025+
expect(subscription).toBeDefined();
4026+
});
4027+
4028+
it('should allow LiveQuery subscription when queryDepth is disabled', async () => {
4029+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
4030+
await reconfigureServer({
4031+
liveQuery: { classNames: ['TestClass'] },
4032+
startLiveQueryServer: true,
4033+
verbose: false,
4034+
silent: true,
4035+
requestComplexity: { queryDepth: -1 },
4036+
});
4037+
const query = new Parse.Query('TestClass');
4038+
let where = { field: 'value' };
4039+
for (let i = 0; i < 15; i++) {
4040+
where = { $or: [where] };
4041+
}
4042+
query._where = where;
4043+
const subscription = await query.subscribe();
4044+
expect(subscription).toBeDefined();
4045+
});
4046+
});

src/LiveQuery/ParseLiveQueryServer.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1023,8 +1023,35 @@ class ParseLiveQueryServer {
10231023
return;
10241024
}
10251025
}
1026-
// Check CLP for subscribe operation
1026+
// Validate query condition depth
10271027
const appConfig = Config.get(this.config.appId);
1028+
if (!client.hasMasterKey) {
1029+
const rc = appConfig.requestComplexity;
1030+
if (rc && rc.queryDepth !== -1) {
1031+
const maxDepth = rc.queryDepth;
1032+
const checkDepth = (where: any, depth: number) => {
1033+
if (depth > maxDepth) {
1034+
throw new Parse.Error(
1035+
Parse.Error.INVALID_QUERY,
1036+
`Query condition nesting depth exceeds maximum allowed depth of ${maxDepth}`
1037+
);
1038+
}
1039+
if (typeof where !== 'object' || where === null) {
1040+
return;
1041+
}
1042+
for (const op of ['$or', '$and', '$nor']) {
1043+
if (Array.isArray(where[op])) {
1044+
for (const subQuery of where[op]) {
1045+
checkDepth(subQuery, depth + 1);
1046+
}
1047+
}
1048+
}
1049+
};
1050+
checkDepth(request.query.where, 0);
1051+
}
1052+
}
1053+
1054+
// Check CLP for subscribe operation
10281055
const schemaController = await appConfig.database.loadSchema();
10291056
const classLevelPermissions = schemaController.getClassLevelPermissions(className);
10301057
const op = this._getCLPOperation(request.query);

0 commit comments

Comments
 (0)