mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Query condition depth bypass via pre-validation transform pipeline ([GHSA-9fjp-q3c4-6w3j](https://github.com/parse-community/parse-server/security/advisories/GHSA-9fjp-q3c4-6w3j)) (#10258)
This commit is contained in:
@@ -2700,6 +2700,90 @@ describe('(GHSA-9xp9-j92r-p88v) Stack overflow process crash via deeply nested q
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects deeply nested query before transform pipeline processes it', async () => {
|
||||
await reconfigureServer({
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const auth = require('../lib/Auth');
|
||||
const rest = require('../lib/rest');
|
||||
const config = Config.get('test');
|
||||
// Depth 50 bypasses the fix because RestQuery.js transform pipeline
|
||||
// recursively traverses the structure before validateQuery() is reached
|
||||
let where = { username: 'test' };
|
||||
for (let i = 0; i < 50; i++) {
|
||||
where = { $and: [where] };
|
||||
}
|
||||
await expectAsync(
|
||||
rest.find(config, auth.nobody(config), '_User', where)
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects deeply nested query via REST API without authentication', async () => {
|
||||
await reconfigureServer({
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
let where = { username: 'test' };
|
||||
for (let i = 0; i < 50; i++) {
|
||||
where = { $or: [where] };
|
||||
}
|
||||
await expectAsync(
|
||||
request({
|
||||
method: 'GET',
|
||||
url: `${Parse.serverURL}/classes/_User`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
qs: { where: JSON.stringify(where) },
|
||||
})
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
data: jasmine.objectContaining({
|
||||
code: Parse.Error.INVALID_QUERY,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects deeply nested $nor query before transform pipeline', async () => {
|
||||
await reconfigureServer({
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const auth = require('../lib/Auth');
|
||||
const rest = require('../lib/rest');
|
||||
const config = Config.get('test');
|
||||
let where = { username: 'test' };
|
||||
for (let i = 0; i < 50; i++) {
|
||||
where = { $nor: [where] };
|
||||
}
|
||||
await expectAsync(
|
||||
rest.find(config, auth.nobody(config), '_User', where)
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('allows queries within the depth limit', async () => {
|
||||
await reconfigureServer({
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const auth = require('../lib/Auth');
|
||||
const rest = require('../lib/rest');
|
||||
const config = Config.get('test');
|
||||
let where = { username: 'test' };
|
||||
for (let i = 0; i < 5; i++) {
|
||||
where = { $or: [where] };
|
||||
}
|
||||
const result = await rest.find(config, auth.nobody(config), '_User', where);
|
||||
expect(result.results).toBeDefined();
|
||||
});
|
||||
|
||||
describe('(GHSA-wjqw-r9x4-j59v) Empty authData session issuance bypass', () => {
|
||||
const signupHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -281,6 +281,9 @@ function _UnsafeRestQuery(
|
||||
// TODO: consolidate the replaceX functions
|
||||
_UnsafeRestQuery.prototype.execute = function (executeOptions) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.validateQueryDepth();
|
||||
})
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
})
|
||||
@@ -352,6 +355,36 @@ _UnsafeRestQuery.prototype.each = function (callback) {
|
||||
);
|
||||
};
|
||||
|
||||
_UnsafeRestQuery.prototype.validateQueryDepth = function () {
|
||||
if (this.auth.isMaster || this.auth.isMaintenance) {
|
||||
return;
|
||||
}
|
||||
const rc = this.config.requestComplexity;
|
||||
if (!rc || rc.queryDepth === -1) {
|
||||
return;
|
||||
}
|
||||
const maxDepth = rc.queryDepth;
|
||||
const checkDepth = (where, depth) => {
|
||||
if (depth > maxDepth) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Query condition nesting depth exceeds maximum allowed depth of ${maxDepth}`
|
||||
);
|
||||
}
|
||||
if (typeof where !== 'object' || where === null) {
|
||||
return;
|
||||
}
|
||||
for (const op of ['$or', '$and', '$nor']) {
|
||||
if (Array.isArray(where[op])) {
|
||||
for (const subQuery of where[op]) {
|
||||
checkDepth(subQuery, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
checkDepth(this.restWhere, 0);
|
||||
};
|
||||
|
||||
_UnsafeRestQuery.prototype.buildRestWhere = function () {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
|
||||
Reference in New Issue
Block a user