Skip to content

Commit 060d270

Browse files
authored
fix: LiveQuery subscription query depth bypass ([GHSA-6qh5-m6g3-xhq6](GHSA-6qh5-m6g3-xhq6)) (#10260)
1 parent ec028e7 commit 060d270

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
@@ -3600,3 +3600,119 @@ describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via Live
36003600
expect(updateSpy).not.toHaveBeenCalled();
36013601
});
36023602
});
3603+
3604+
describe('(GHSA-6qh5-m6g3-xhq6) LiveQuery query depth DoS via deeply nested subscription', () => {
3605+
afterEach(async () => {
3606+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
3607+
if (client) {
3608+
await client.close();
3609+
}
3610+
});
3611+
3612+
it('should reject LiveQuery subscription with deeply nested $or when queryDepth is set', async () => {
3613+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3614+
await reconfigureServer({
3615+
liveQuery: { classNames: ['TestClass'] },
3616+
startLiveQueryServer: true,
3617+
verbose: false,
3618+
silent: true,
3619+
requestComplexity: { queryDepth: 10 },
3620+
});
3621+
const query = new Parse.Query('TestClass');
3622+
let where = { field: 'value' };
3623+
for (let i = 0; i < 15; i++) {
3624+
where = { $or: [where] };
3625+
}
3626+
query._where = where;
3627+
await expectAsync(query.subscribe()).toBeRejectedWith(
3628+
jasmine.objectContaining({
3629+
code: Parse.Error.INVALID_QUERY,
3630+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
3631+
})
3632+
);
3633+
});
3634+
3635+
it('should reject LiveQuery subscription with deeply nested $and when queryDepth is set', async () => {
3636+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3637+
await reconfigureServer({
3638+
liveQuery: { classNames: ['TestClass'] },
3639+
startLiveQueryServer: true,
3640+
verbose: false,
3641+
silent: true,
3642+
requestComplexity: { queryDepth: 10 },
3643+
});
3644+
const query = new Parse.Query('TestClass');
3645+
let where = { field: 'value' };
3646+
for (let i = 0; i < 50; i++) {
3647+
where = { $and: [where] };
3648+
}
3649+
query._where = where;
3650+
await expectAsync(query.subscribe()).toBeRejectedWith(
3651+
jasmine.objectContaining({
3652+
code: Parse.Error.INVALID_QUERY,
3653+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
3654+
})
3655+
);
3656+
});
3657+
3658+
it('should reject LiveQuery subscription with deeply nested $nor when queryDepth is set', async () => {
3659+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3660+
await reconfigureServer({
3661+
liveQuery: { classNames: ['TestClass'] },
3662+
startLiveQueryServer: true,
3663+
verbose: false,
3664+
silent: true,
3665+
requestComplexity: { queryDepth: 10 },
3666+
});
3667+
const query = new Parse.Query('TestClass');
3668+
let where = { field: 'value' };
3669+
for (let i = 0; i < 50; i++) {
3670+
where = { $nor: [where] };
3671+
}
3672+
query._where = where;
3673+
await expectAsync(query.subscribe()).toBeRejectedWith(
3674+
jasmine.objectContaining({
3675+
code: Parse.Error.INVALID_QUERY,
3676+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
3677+
})
3678+
);
3679+
});
3680+
3681+
it('should allow LiveQuery subscription within the depth limit', async () => {
3682+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3683+
await reconfigureServer({
3684+
liveQuery: { classNames: ['TestClass'] },
3685+
startLiveQueryServer: true,
3686+
verbose: false,
3687+
silent: true,
3688+
requestComplexity: { queryDepth: 10 },
3689+
});
3690+
const query = new Parse.Query('TestClass');
3691+
let where = { field: 'value' };
3692+
for (let i = 0; i < 5; i++) {
3693+
where = { $or: [where] };
3694+
}
3695+
query._where = where;
3696+
const subscription = await query.subscribe();
3697+
expect(subscription).toBeDefined();
3698+
});
3699+
3700+
it('should allow LiveQuery subscription when queryDepth is disabled', async () => {
3701+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
3702+
await reconfigureServer({
3703+
liveQuery: { classNames: ['TestClass'] },
3704+
startLiveQueryServer: true,
3705+
verbose: false,
3706+
silent: true,
3707+
requestComplexity: { queryDepth: -1 },
3708+
});
3709+
const query = new Parse.Query('TestClass');
3710+
let where = { field: 'value' };
3711+
for (let i = 0; i < 15; i++) {
3712+
where = { $or: [where] };
3713+
}
3714+
query._where = where;
3715+
const subscription = await query.subscribe();
3716+
expect(subscription).toBeDefined();
3717+
});
3718+
});

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)