mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Classes _GraphQLConfig and _Audience master key bypass via generic class routes ([GHSA-7xg7-rqf6-pw6c](https://github.com/parse-community/parse-server/security/advisories/GHSA-7xg7-rqf6-pw6c)) (#10151)
This commit is contained in:
+12
-11
@@ -36,9 +36,9 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -78,9 +78,9 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -119,9 +119,9 @@ describe('AudiencesRouter', () => {
|
||||
Config.get('test');
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -159,8 +159,8 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest))
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => rest.create(config, auth.master(config), '_Audience', iosAudienceRequest))
|
||||
.then(() => router.handleFind(request))
|
||||
.then(res => {
|
||||
const response = res.response;
|
||||
@@ -197,9 +197,9 @@ describe('AudiencesRouter', () => {
|
||||
|
||||
const router = new AudiencesRouter();
|
||||
rest
|
||||
.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
|
||||
.create(config, auth.master(config), '_Audience', androidAudienceRequest)
|
||||
.then(() => {
|
||||
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
|
||||
return rest.create(config, auth.master(config), '_Audience', iosAudienceRequest);
|
||||
})
|
||||
.then(() => {
|
||||
return router.handleFind(request);
|
||||
@@ -421,6 +421,7 @@ describe('AudiencesRouter', () => {
|
||||
await reconfigureServer({
|
||||
appId: 'test',
|
||||
restAPIKey: 'test',
|
||||
masterKey: 'test',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
try {
|
||||
@@ -430,7 +431,7 @@ describe('AudiencesRouter', () => {
|
||||
body: { lorem: 'ipsum', _method: 'POST' },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'test',
|
||||
'X-Parse-Master-Key': 'test',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1074,7 +1074,7 @@ describe('PushController', () => {
|
||||
const audience = new Parse.Object('_Audience');
|
||||
audience.set('name', 'testAudience');
|
||||
audience.set('query', JSON.stringify(where));
|
||||
await Parse.Object.saveAll(audience);
|
||||
await audience.save(null, { useMasterKey: true });
|
||||
await query.find({ useMasterKey: true }).then(parseResults);
|
||||
|
||||
const body = {
|
||||
|
||||
@@ -812,6 +812,153 @@ describe('rest create', () => {
|
||||
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _GlobalConfig collection."));
|
||||
});
|
||||
|
||||
it('should require master key for all volatile classes', () => {
|
||||
// This test guards against drift between volatileClasses (SchemaController.js)
|
||||
// and classesWithMasterOnlyAccess (SharedRest.js). If a new volatile class is
|
||||
// added, it must also be added to classesWithMasterOnlyAccess and this test.
|
||||
const volatileClasses = [
|
||||
'_JobStatus',
|
||||
'_PushStatus',
|
||||
'_Hooks',
|
||||
'_GlobalConfig',
|
||||
'_GraphQLConfig',
|
||||
'_JobSchedule',
|
||||
'_Audience',
|
||||
'_Idempotency',
|
||||
];
|
||||
for (const className of volatileClasses) {
|
||||
expect(() =>
|
||||
rest.create(config, auth.nobody(config), className, {})
|
||||
).toThrowMatching(
|
||||
e => e.code === Parse.Error.OPERATION_FORBIDDEN,
|
||||
`Expected ${className} to require master key`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot find objects in _GraphQLConfig without masterKey', async () => {
|
||||
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
|
||||
await expectAsync(
|
||||
rest.find(config, auth.nobody(config), '_GraphQLConfig', {})
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN })
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot update object in _GraphQLConfig without masterKey', async () => {
|
||||
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
|
||||
expect(() =>
|
||||
rest.update(config, auth.nobody(config), '_GraphQLConfig', '1', {
|
||||
config: { enabledForClasses: [] },
|
||||
})
|
||||
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
|
||||
});
|
||||
|
||||
it('cannot delete object in _GraphQLConfig without masterKey', async () => {
|
||||
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
|
||||
expect(() =>
|
||||
rest.del(config, auth.nobody(config), '_GraphQLConfig', '1')
|
||||
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
|
||||
});
|
||||
|
||||
it('can perform operations on _GraphQLConfig with masterKey', async () => {
|
||||
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
|
||||
const found = await rest.find(config, auth.master(config), '_GraphQLConfig', {});
|
||||
expect(found.results.length).toBeGreaterThan(0);
|
||||
await rest.del(config, auth.master(config), '_GraphQLConfig', '1');
|
||||
const afterDelete = await rest.find(config, auth.master(config), '_GraphQLConfig', {});
|
||||
expect(afterDelete.results.length).toBe(0);
|
||||
});
|
||||
|
||||
it('cannot create object in _Audience without masterKey', () => {
|
||||
expect(() =>
|
||||
rest.create(config, auth.nobody(config), '_Audience', {
|
||||
name: 'test',
|
||||
query: '{}',
|
||||
})
|
||||
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
|
||||
});
|
||||
|
||||
it('cannot find objects in _Audience without masterKey', async () => {
|
||||
await expectAsync(
|
||||
rest.find(config, auth.nobody(config), '_Audience', {})
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN })
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot update object in _Audience without masterKey', async () => {
|
||||
const obj = await rest.create(config, auth.master(config), '_Audience', {
|
||||
name: 'test',
|
||||
query: '{}',
|
||||
});
|
||||
expect(() =>
|
||||
rest.update(config, auth.nobody(config), '_Audience', obj.response.objectId, {
|
||||
name: 'updated',
|
||||
})
|
||||
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
|
||||
});
|
||||
|
||||
it('cannot delete object in _Audience without masterKey', async () => {
|
||||
const obj = await rest.create(config, auth.master(config), '_Audience', {
|
||||
name: 'test',
|
||||
query: '{}',
|
||||
});
|
||||
expect(() =>
|
||||
rest.del(config, auth.nobody(config), '_Audience', obj.response.objectId)
|
||||
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
|
||||
});
|
||||
|
||||
it('can perform CRUD on _Audience with masterKey', async () => {
|
||||
const obj = await rest.create(config, auth.master(config), '_Audience', {
|
||||
name: 'test',
|
||||
query: '{}',
|
||||
});
|
||||
expect(obj.response.objectId).toBeDefined();
|
||||
const found = await rest.find(config, auth.master(config), '_Audience', {});
|
||||
expect(found.results.length).toBeGreaterThan(0);
|
||||
await rest.del(config, auth.master(config), '_Audience', obj.response.objectId);
|
||||
const afterDelete = await rest.find(config, auth.master(config), '_Audience', {});
|
||||
expect(afterDelete.results.length).toBe(0);
|
||||
});
|
||||
|
||||
it('cannot access _GraphQLConfig via class route without masterKey', async () => {
|
||||
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
|
||||
try {
|
||||
await request({
|
||||
url: 'http://localhost:8378/1/classes/_GraphQLConfig',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
it('cannot access _Audience via class route without masterKey', async () => {
|
||||
await rest.create(config, auth.master(config), '_Audience', {
|
||||
name: 'test',
|
||||
query: '{}',
|
||||
});
|
||||
try {
|
||||
await request({
|
||||
url: 'http://localhost:8378/1/classes/_Audience',
|
||||
json: true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
},
|
||||
});
|
||||
fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
it('locks down session', done => {
|
||||
let currentUser;
|
||||
Parse.User.signUp('foo', 'bar')
|
||||
|
||||
@@ -3,7 +3,9 @@ const classesWithMasterOnlyAccess = [
|
||||
'_PushStatus',
|
||||
'_Hooks',
|
||||
'_GlobalConfig',
|
||||
'_GraphQLConfig',
|
||||
'_JobSchedule',
|
||||
'_Audience',
|
||||
'_Idempotency',
|
||||
];
|
||||
const { createSanitizedError } = require('./Error');
|
||||
|
||||
Reference in New Issue
Block a user