Skip to content

Commit 85994ef

Browse files
authored
fix: Query condition depth bypass via pre-validation transform pipeline ([GHSA-9fjp-q3c4-6w3j](GHSA-9fjp-q3c4-6w3j)) (#10257)
1 parent 683e2ae commit 85994ef

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
@@ -2833,6 +2833,90 @@ describe('(GHSA-9xp9-j92r-p88v) Stack overflow process crash via deeply nested q
28332833
})
28342834
);
28352835
});
2836+
2837+
it('rejects deeply nested query before transform pipeline processes it', async () => {
2838+
await reconfigureServer({
2839+
requestComplexity: { queryDepth: 10 },
2840+
});
2841+
const auth = require('../lib/Auth');
2842+
const rest = require('../lib/rest');
2843+
const config = Config.get('test');
2844+
// Depth 50 bypasses the fix because RestQuery.js transform pipeline
2845+
// recursively traverses the structure before validateQuery() is reached
2846+
let where = { username: 'test' };
2847+
for (let i = 0; i < 50; i++) {
2848+
where = { $and: [where] };
2849+
}
2850+
await expectAsync(
2851+
rest.find(config, auth.nobody(config), '_User', where)
2852+
).toBeRejectedWith(
2853+
jasmine.objectContaining({
2854+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
2855+
})
2856+
);
2857+
});
2858+
2859+
it('rejects deeply nested query via REST API without authentication', async () => {
2860+
await reconfigureServer({
2861+
requestComplexity: { queryDepth: 10 },
2862+
});
2863+
let where = { username: 'test' };
2864+
for (let i = 0; i < 50; i++) {
2865+
where = { $or: [where] };
2866+
}
2867+
await expectAsync(
2868+
request({
2869+
method: 'GET',
2870+
url: `${Parse.serverURL}/classes/_User`,
2871+
headers: {
2872+
'X-Parse-Application-Id': Parse.applicationId,
2873+
'X-Parse-REST-API-Key': 'rest',
2874+
},
2875+
qs: { where: JSON.stringify(where) },
2876+
})
2877+
).toBeRejectedWith(
2878+
jasmine.objectContaining({
2879+
data: jasmine.objectContaining({
2880+
code: Parse.Error.INVALID_QUERY,
2881+
}),
2882+
})
2883+
);
2884+
});
2885+
2886+
it('rejects deeply nested $nor query before transform pipeline', async () => {
2887+
await reconfigureServer({
2888+
requestComplexity: { queryDepth: 10 },
2889+
});
2890+
const auth = require('../lib/Auth');
2891+
const rest = require('../lib/rest');
2892+
const config = Config.get('test');
2893+
let where = { username: 'test' };
2894+
for (let i = 0; i < 50; i++) {
2895+
where = { $nor: [where] };
2896+
}
2897+
await expectAsync(
2898+
rest.find(config, auth.nobody(config), '_User', where)
2899+
).toBeRejectedWith(
2900+
jasmine.objectContaining({
2901+
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
2902+
})
2903+
);
2904+
});
2905+
2906+
it('allows queries within the depth limit', async () => {
2907+
await reconfigureServer({
2908+
requestComplexity: { queryDepth: 10 },
2909+
});
2910+
const auth = require('../lib/Auth');
2911+
const rest = require('../lib/rest');
2912+
const config = Config.get('test');
2913+
let where = { username: 'test' };
2914+
for (let i = 0; i < 5; i++) {
2915+
where = { $or: [where] };
2916+
}
2917+
const result = await rest.find(config, auth.nobody(config), '_User', where);
2918+
expect(result.results).toBeDefined();
2919+
});
28362920
});
28372921

28382922
describe('(GHSA-fjxm-vhvc-gcmj) LiveQuery Operator Type Confusion', () => {

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)