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:
Manuel
2026-03-20 17:43:37 +00:00
committed by GitHub
parent 9cda64fdc0
commit 2581b54260
2 changed files with 117 additions and 0 deletions
+84
View File
@@ -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',
+33
View File
@@ -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(() => {