mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e397b83c51 | ||
|
|
26b628c8fb | ||
|
|
a59a60794e | ||
|
|
060d27053f | ||
|
|
ec028e7354 | ||
|
|
2581b54260 | ||
|
|
9cda64fdc0 | ||
|
|
c62eacaf38 |
@@ -1,3 +1,31 @@
|
||||
## [8.6.57](https://github.com/parse-community/parse-server/compare/8.6.56...8.6.57) (2026-03-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Session update endpoint allows overwriting server-generated session fields ([GHSA-jc39-686j-wp6q](https://github.com/parse-community/parse-server/security/advisories/GHSA-jc39-686j-wp6q)) ([#10264](https://github.com/parse-community/parse-server/issues/10264)) ([26b628c](https://github.com/parse-community/parse-server/commit/26b628c8fb3cc79ea955374769eebcff6f8a8a73))
|
||||
|
||||
## [8.6.56](https://github.com/parse-community/parse-server/compare/8.6.55...8.6.56) (2026-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* LiveQuery subscription query depth bypass ([GHSA-6qh5-m6g3-xhq6](https://github.com/parse-community/parse-server/security/advisories/GHSA-6qh5-m6g3-xhq6)) ([#10260](https://github.com/parse-community/parse-server/issues/10260)) ([060d270](https://github.com/parse-community/parse-server/commit/060d27053fb0fadf613c25aabab7fe0c82b7a899))
|
||||
|
||||
## [8.6.55](https://github.com/parse-community/parse-server/compare/8.6.54...8.6.55) (2026-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 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](https://github.com/parse-community/parse-server/issues/10258)) ([2581b54](https://github.com/parse-community/parse-server/commit/2581b5426047ce9cbcd3d9c0e8379e9c30e23ab5))
|
||||
|
||||
## [8.6.54](https://github.com/parse-community/parse-server/compare/8.6.53...8.6.54) (2026-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Protected field change detection oracle via LiveQuery watch parameter ([GHSA-qpc3-fg4j-8hgm](https://github.com/parse-community/parse-server/security/advisories/GHSA-qpc3-fg4j-8hgm)) ([#10254](https://github.com/parse-community/parse-server/issues/10254)) ([c62eaca](https://github.com/parse-community/parse-server/commit/c62eacaf38de86913f09240583448360b1cc8e67))
|
||||
|
||||
## [8.6.53](https://github.com/parse-community/parse-server/compare/8.6.52...8.6.53) (2026-03-20)
|
||||
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.6.53",
|
||||
"version": "8.6.57",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "parse-server",
|
||||
"version": "8.6.53",
|
||||
"version": "8.6.57",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "8.6.53",
|
||||
"version": "8.6.57",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -256,4 +256,141 @@ describe('Parse.Session', () => {
|
||||
expect(newSession.createdWith.action).toBe('create');
|
||||
expect(newSession.createdWith.authProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reject expiresAt when updating a session via PUT', async () => {
|
||||
const user = await Parse.User.signUp('sessionupdateuser1', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
|
||||
// Get the session objectId
|
||||
const sessionRes = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
});
|
||||
const sessionId = sessionRes.data.objectId;
|
||||
const originalExpiresAt = sessionRes.data.expiresAt;
|
||||
|
||||
// Attempt to overwrite expiresAt via PUT
|
||||
const updateRes = await request({
|
||||
method: 'PUT',
|
||||
url: `http://localhost:8378/1/sessions/${sessionId}`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
expiresAt: { __type: 'Date', iso: '2099-12-31T23:59:59.000Z' },
|
||||
},
|
||||
}).catch(e => e);
|
||||
|
||||
expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
|
||||
// Verify expiresAt was not changed
|
||||
const verifyRes = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
});
|
||||
expect(verifyRes.data.expiresAt).toEqual(originalExpiresAt);
|
||||
});
|
||||
|
||||
it('should reject createdWith when updating a session via PUT', async () => {
|
||||
const user = await Parse.User.signUp('sessionupdateuser2', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
|
||||
// Get the session objectId
|
||||
const sessionRes = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
});
|
||||
const sessionId = sessionRes.data.objectId;
|
||||
const originalCreatedWith = sessionRes.data.createdWith;
|
||||
|
||||
// Attempt to overwrite createdWith via PUT
|
||||
const updateRes = await request({
|
||||
method: 'PUT',
|
||||
url: `http://localhost:8378/1/sessions/${sessionId}`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
createdWith: { action: 'attacker', authProvider: 'evil' },
|
||||
},
|
||||
}).catch(e => e);
|
||||
|
||||
expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME);
|
||||
|
||||
// Verify createdWith was not changed
|
||||
const verifyRes = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
});
|
||||
expect(verifyRes.data.createdWith).toEqual(originalCreatedWith);
|
||||
});
|
||||
|
||||
it('should allow master key to update expiresAt on a session', async () => {
|
||||
const user = await Parse.User.signUp('sessionupdateuser3', 'password');
|
||||
const sessionToken = user.getSessionToken();
|
||||
|
||||
// Get the session objectId
|
||||
const sessionRes = await request({
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8378/1/sessions/me',
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'X-Parse-Session-Token': sessionToken,
|
||||
},
|
||||
});
|
||||
const sessionId = sessionRes.data.objectId;
|
||||
const farFuture = '2099-12-31T23:59:59.000Z';
|
||||
|
||||
// Master key should be able to update expiresAt
|
||||
await request({
|
||||
method: 'PUT',
|
||||
url: `http://localhost:8378/1/sessions/${sessionId}`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
expiresAt: { __type: 'Date', iso: farFuture },
|
||||
},
|
||||
});
|
||||
|
||||
// Verify expiresAt was changed
|
||||
const verifyRes = await request({
|
||||
method: 'GET',
|
||||
url: `http://localhost:8378/1/sessions/${sessionId}`,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
},
|
||||
});
|
||||
expect(verifyRes.data.expiresAt.iso).toBe(farFuture);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
@@ -3416,3 +3500,219 @@ describe('(GHSA-fph2-r4qg-9576) LiveQuery bypasses CLP pointer permission enforc
|
||||
expect(createSpyC).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('(GHSA-qpc3-fg4j-8hgm) Protected field change detection oracle via LiveQuery watch parameter', () => {
|
||||
const { sleep } = require('../lib/TestUtils');
|
||||
let obj;
|
||||
|
||||
beforeEach(async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['SecretClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const schemaController = await config.database.loadSchema();
|
||||
await schemaController.addClassIfNotExists('SecretClass', {
|
||||
secretObj: { type: 'Object' },
|
||||
publicField: { type: 'String' },
|
||||
});
|
||||
await schemaController.updateClass(
|
||||
'SecretClass',
|
||||
{},
|
||||
{
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: {},
|
||||
protectedFields: { '*': ['secretObj'] },
|
||||
}
|
||||
);
|
||||
|
||||
obj = new Parse.Object('SecretClass');
|
||||
obj.set('secretObj', { apiKey: 'SENSITIVE_KEY_123', score: 42 });
|
||||
obj.set('publicField', 'visible');
|
||||
await obj.save(null, { useMasterKey: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject LiveQuery subscription with protected field in watch', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.watch('secretObj');
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject LiveQuery subscription with dot-notation on protected field in watch', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.watch('secretObj.apiKey');
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject LiveQuery subscription with deeply nested dot-notation on protected field in watch', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.watch('secretObj.nested.deep.key');
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow LiveQuery subscription with non-protected field in watch', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.watch('publicField');
|
||||
const subscription = await query.subscribe();
|
||||
await Promise.all([
|
||||
new Promise(resolve => {
|
||||
subscription.on('update', object => {
|
||||
expect(object.get('secretObj')).toBeUndefined();
|
||||
expect(object.get('publicField')).toBe('updated');
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
obj.save({ publicField: 'updated' }, { useMasterKey: true }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not deliver update event when only non-watched field changes', async () => {
|
||||
const query = new Parse.Query('SecretClass');
|
||||
query.watch('publicField');
|
||||
const subscription = await query.subscribe();
|
||||
const updateSpy = jasmine.createSpy('update');
|
||||
subscription.on('update', updateSpy);
|
||||
|
||||
// Change a field that is NOT in the watch list
|
||||
obj.set('secretObj', { apiKey: 'ROTATED_KEY', score: 99 });
|
||||
await obj.save(null, { useMasterKey: true });
|
||||
await sleep(500);
|
||||
expect(updateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('(GHSA-6qh5-m6g3-xhq6) LiveQuery query depth DoS via deeply nested subscription', () => {
|
||||
afterEach(async () => {
|
||||
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
||||
if (client) {
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject LiveQuery subscription with deeply nested $or when queryDepth is set', async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['TestClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const query = new Parse.Query('TestClass');
|
||||
let where = { field: 'value' };
|
||||
for (let i = 0; i < 15; i++) {
|
||||
where = { $or: [where] };
|
||||
}
|
||||
query._where = where;
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
code: Parse.Error.INVALID_QUERY,
|
||||
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject LiveQuery subscription with deeply nested $and when queryDepth is set', async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['TestClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const query = new Parse.Query('TestClass');
|
||||
let where = { field: 'value' };
|
||||
for (let i = 0; i < 50; i++) {
|
||||
where = { $and: [where] };
|
||||
}
|
||||
query._where = where;
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
code: Parse.Error.INVALID_QUERY,
|
||||
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject LiveQuery subscription with deeply nested $nor when queryDepth is set', async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['TestClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const query = new Parse.Query('TestClass');
|
||||
let where = { field: 'value' };
|
||||
for (let i = 0; i < 50; i++) {
|
||||
where = { $nor: [where] };
|
||||
}
|
||||
query._where = where;
|
||||
await expectAsync(query.subscribe()).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
code: Parse.Error.INVALID_QUERY,
|
||||
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth/),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow LiveQuery subscription within the depth limit', async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['TestClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
requestComplexity: { queryDepth: 10 },
|
||||
});
|
||||
const query = new Parse.Query('TestClass');
|
||||
let where = { field: 'value' };
|
||||
for (let i = 0; i < 5; i++) {
|
||||
where = { $or: [where] };
|
||||
}
|
||||
query._where = where;
|
||||
const subscription = await query.subscribe();
|
||||
expect(subscription).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow LiveQuery subscription when queryDepth is disabled', async () => {
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
|
||||
await reconfigureServer({
|
||||
liveQuery: { classNames: ['TestClass'] },
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
requestComplexity: { queryDepth: -1 },
|
||||
});
|
||||
const query = new Parse.Query('TestClass');
|
||||
let where = { field: 'value' };
|
||||
for (let i = 0; i < 15; i++) {
|
||||
where = { $or: [where] };
|
||||
}
|
||||
query._where = where;
|
||||
const subscription = await query.subscribe();
|
||||
expect(subscription).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1023,8 +1023,35 @@ class ParseLiveQueryServer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check CLP for subscribe operation
|
||||
// Validate query condition depth
|
||||
const appConfig = Config.get(this.config.appId);
|
||||
if (!client.hasMasterKey) {
|
||||
const rc = appConfig.requestComplexity;
|
||||
if (rc && rc.queryDepth !== -1) {
|
||||
const maxDepth = rc.queryDepth;
|
||||
const checkDepth = (where: any, depth: number) => {
|
||||
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(request.query.where, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check CLP for subscribe operation
|
||||
const schemaController = await appConfig.database.loadSchema();
|
||||
const classLevelPermissions = schemaController.getClassLevelPermissions(className);
|
||||
const op = this._getCLPOperation(request.query);
|
||||
@@ -1050,7 +1077,7 @@ class ParseLiveQueryServer {
|
||||
op
|
||||
);
|
||||
|
||||
// Check protected fields in WHERE clause
|
||||
// Check protected fields in WHERE clause and WATCH parameter
|
||||
if (!client.hasMasterKey) {
|
||||
const auth = request.user ? { user: request.user, userRoles: [] } : {};
|
||||
const protectedFields =
|
||||
@@ -1083,6 +1110,17 @@ class ParseLiveQueryServer {
|
||||
};
|
||||
checkWhere(request.query.where);
|
||||
}
|
||||
if (protectedFields.length > 0 && Array.isArray(request.query.watch)) {
|
||||
for (const watchField of request.query.watch) {
|
||||
const rootField = watchField.split('.')[0];
|
||||
if (protectedFields.includes(watchField) || protectedFields.includes(rootField)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Permission denied'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate regex patterns in the subscription query
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1147,6 +1147,10 @@ RestWrite.prototype.handleSession = function () {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME);
|
||||
} else if (this.data.sessionToken) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME);
|
||||
} else if (this.data.expiresAt && !this.auth.isMaster && !this.auth.isMaintenance) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME);
|
||||
} else if (this.data.createdWith && !this.auth.isMaster && !this.auth.isMaintenance) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME);
|
||||
}
|
||||
if (!this.auth.isMaster) {
|
||||
this.query = {
|
||||
|
||||
Reference in New Issue
Block a user