Skip to content

Commit 2581b54

Browse files
authored
fix: Query condition depth bypass via pre-validation transform pipeline ([GHSA-9fjp-q3c4-6w3j](GHSA-9fjp-q3c4-6w3j)) (#10258)
1 parent 9cda64f commit 2581b54

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed

spec/vulnerabilities.spec.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2700,6 +2700,90 @@ describe('(GHSA-9xp9-j92r-p88v) Stack overflow process crash via deeply nested q
27002700
);
27012701
});
27022702

2703+
it('rejects deeply nested query before transform pipeline processes it', async () => {
2704+
await reconfigureServer({
2705+
requestComplexity: { queryDepth: 10 },
2706+
});
2707+
const auth = require('../lib/Auth');
2708+
const rest = require('../lib/rest');
2709+
const config = Config.get('test');
2710+
// Depth 50 bypasses the fix because RestQuery.js transform pipeline
2711+
// recursively traverses the structure before validateQuery() is reached
2712+
let where = { username: 'test' };
2713+
for (let i = 0; i < 50; i++) {
2714+
where = { $and: [where] };
2715+
}
2716+
await expectAsync(
2717+
rest.find(config, auth.nobody(config), '_User', where)
2718+
).toBeRejectedWith(
2719+
jasmine.objectContaining({
2720+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
2721+
})
2722+
);
2723+
});
2724+
2725+
it('rejects deeply nested query via REST API without authentication', async () => {
2726+
await reconfigureServer({
2727+
requestComplexity: { queryDepth: 10 },
2728+
});
2729+
let where = { username: 'test' };
2730+
for (let i = 0; i < 50; i++) {
2731+
where = { $or: [where] };
2732+
}
2733+
await expectAsync(
2734+
request({
2735+
method: 'GET',
2736+
url: `${Parse.serverURL}/classes/_User`,
2737+
headers: {
2738+
'X-Parse-Application-Id': Parse.applicationId,
2739+
'X-Parse-REST-API-Key': 'rest',
2740+
},
2741+
qs: { where: JSON.stringify(where) },
2742+
})
2743+
).toBeRejectedWith(
2744+
jasmine.objectContaining({
2745+
data: jasmine.objectContaining({
2746+
code: Parse.Error.INVALID_QUERY,
2747+
}),
2748+
})
2749+
);
2750+
});
2751+
2752+
it('rejects deeply nested $nor query before transform pipeline', async () => {
2753+
await reconfigureServer({
2754+
requestComplexity: { queryDepth: 10 },
2755+
});
2756+
const auth = require('../lib/Auth');
2757+
const rest = require('../lib/rest');
2758+
const config = Config.get('test');
2759+
let where = { username: 'test' };
2760+
for (let i = 0; i < 50; i++) {
2761+
where = { $nor: [where] };
2762+
}
2763+
await expectAsync(
2764+
rest.find(config, auth.nobody(config), '_User', where)
2765+
).toBeRejectedWith(
2766+
jasmine.objectContaining({
2767+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
2768+
})
2769+
);
2770+
});
2771+
2772+
it('allows queries within the depth limit', async () => {
2773+
await reconfigureServer({
2774+
requestComplexity: { queryDepth: 10 },
2775+
});
2776+
const auth = require('../lib/Auth');
2777+
const rest = require('../lib/rest');
2778+
const config = Config.get('test');
2779+
let where = { username: 'test' };
2780+
for (let i = 0; i < 5; i++) {
2781+
where = { $or: [where] };
2782+
}
2783+
const result = await rest.find(config, auth.nobody(config), '_User', where);
2784+
expect(result.results).toBeDefined();
2785+
});
2786+
27032787
describe('(GHSA-wjqw-r9x4-j59v) Empty authData session issuance bypass', () => {
27042788
const signupHeaders = {
27052789
'Content-Type': 'application/json',

src/RestQuery.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ function _UnsafeRestQuery(
281281
// TODO: consolidate the replaceX functions
282282
_UnsafeRestQuery.prototype.execute = function (executeOptions) {
283283
return Promise.resolve()
284+
.then(() => {
285+
return this.validateQueryDepth();
286+
})
284287
.then(() => {
285288
return this.buildRestWhere();
286289
})
@@ -352,6 +355,36 @@ _UnsafeRestQuery.prototype.each = function (callback) {
352355
);
353356
};
354357

358+
_UnsafeRestQuery.prototype.validateQueryDepth = function () {
359+
if (this.auth.isMaster || this.auth.isMaintenance) {
360+
return;
361+
}
362+
const rc = this.config.requestComplexity;
363+
if (!rc || rc.queryDepth === -1) {
364+
return;
365+
}
366+
const maxDepth = rc.queryDepth;
367+
const checkDepth = (where, depth) => {
368+
if (depth > maxDepth) {
369+
throw new Parse.Error(
370+
Parse.Error.INVALID_QUERY,
371+
`Query condition nesting depth exceeds maximum allowed depth of ${maxDepth}`
372+
);
373+
}
374+
if (typeof where !== 'object' || where === null) {
375+
return;
376+
}
377+
for (const op of ['$or', '$and', '$nor']) {
378+
if (Array.isArray(where[op])) {
379+
for (const subQuery of where[op]) {
380+
checkDepth(subQuery, depth + 1);
381+
}
382+
}
383+
}
384+
};
385+
checkDepth(this.restWhere, 0);
386+
};
387+
355388
_UnsafeRestQuery.prototype.buildRestWhere = function () {
356389
return Promise.resolve()
357390
.then(() => {

0 commit comments

Comments
 (0)