mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Parse Server role escalation and CLP bypass via direct `_Join table write ([GHSA-5f92-jrq3-28rc](https://github.com/parse-community/parse-server/security/advisories/GHSA-5f92-jrq3-28rc)) (#10142)
This commit is contained in:
@@ -247,6 +247,9 @@ global.afterEachFn = async () => {
|
||||
if (!className.startsWith('_')) {
|
||||
return true;
|
||||
}
|
||||
if (className.startsWith('_Join:')) {
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
'_User',
|
||||
'_Installation',
|
||||
|
||||
@@ -953,6 +953,117 @@ describe('rest update', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_Join table security', () => {
|
||||
let config;
|
||||
|
||||
beforeEach(() => {
|
||||
config = Config.get('test');
|
||||
});
|
||||
|
||||
it('cannot create object in _Join table without masterKey', () => {
|
||||
expect(() =>
|
||||
rest.create(config, auth.nobody(config), '_Join:users:_Role', {
|
||||
relatedId: 'someUserId',
|
||||
owningId: 'someRoleId',
|
||||
})
|
||||
).toThrowError(/Permission denied/);
|
||||
});
|
||||
|
||||
it('cannot find objects in _Join table without masterKey', async () => {
|
||||
await expectAsync(
|
||||
rest.find(config, auth.nobody(config), '_Join:users:_Role', {})
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
code: Parse.Error.OPERATION_FORBIDDEN,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('cannot update object in _Join table without masterKey', () => {
|
||||
expect(() =>
|
||||
rest.update(config, auth.nobody(config), '_Join:users:_Role', { relatedId: 'someUserId' }, { owningId: 'newRoleId' })
|
||||
).toThrowError(/Permission denied/);
|
||||
});
|
||||
|
||||
it('cannot delete object in _Join table without masterKey', () => {
|
||||
expect(() =>
|
||||
rest.del(config, auth.nobody(config), '_Join:users:_Role', 'someObjectId')
|
||||
).toThrowError(/Permission denied/);
|
||||
});
|
||||
|
||||
it('cannot get object in _Join table without masterKey', async () => {
|
||||
await expectAsync(
|
||||
rest.get(config, auth.nobody(config), '_Join:users:_Role', 'someObjectId')
|
||||
).toBeRejectedWith(
|
||||
jasmine.objectContaining({
|
||||
code: Parse.Error.OPERATION_FORBIDDEN,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('can find objects in _Join table with masterKey', async () => {
|
||||
await expectAsync(
|
||||
rest.find(config, auth.master(config), '_Join:users:_Role', {})
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('can find objects in _Join table with maintenance key', async () => {
|
||||
await expectAsync(
|
||||
rest.find(config, auth.maintenance(config), '_Join:users:_Role', {})
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('legitimate relation operations still work', async () => {
|
||||
const role = new Parse.Role('admin', new Parse.ACL());
|
||||
const user = await Parse.User.signUp('testuser', 'password123');
|
||||
role.getUsers().add(user);
|
||||
await role.save(null, { useMasterKey: true });
|
||||
const result = await rest.find(config, auth.master(config), '_Join:users:_Role', {});
|
||||
expect(result.results.length).toBe(1);
|
||||
});
|
||||
|
||||
it('blocks _Join table access for any relation, not just _Role', () => {
|
||||
expect(() =>
|
||||
rest.create(config, auth.nobody(config), '_Join:viewers:ConfidentialDoc', {
|
||||
relatedId: 'someUserId',
|
||||
owningId: 'someDocId',
|
||||
})
|
||||
).toThrowError(/Permission denied/);
|
||||
});
|
||||
|
||||
it('cannot escalate role via direct _Join table write', async () => {
|
||||
const role = new Parse.Role('superadmin', new Parse.ACL());
|
||||
await role.save(null, { useMasterKey: true });
|
||||
const user = await Parse.User.signUp('attacker', 'password123');
|
||||
const sessionToken = user.getSessionToken();
|
||||
const userAuth = await auth.getAuthForSessionToken({
|
||||
config,
|
||||
sessionToken,
|
||||
});
|
||||
expect(() =>
|
||||
rest.create(config, userAuth, '_Join:users:_Role', {
|
||||
relatedId: user.id,
|
||||
owningId: role.id,
|
||||
})
|
||||
).toThrowError(/Permission denied/);
|
||||
});
|
||||
|
||||
it('cannot write to _Join table with read-only masterKey', () => {
|
||||
expect(() =>
|
||||
rest.create(config, auth.readOnly(config), '_Join:users:_Role', {
|
||||
relatedId: 'someUserId',
|
||||
owningId: 'someRoleId',
|
||||
})
|
||||
).toThrowError(/Permission denied/);
|
||||
});
|
||||
|
||||
it('can read _Join table with read-only masterKey', async () => {
|
||||
await expectAsync(
|
||||
rest.find(config, auth.readOnly(config), '_Join:users:_Role', {})
|
||||
).toBeResolved();
|
||||
});
|
||||
});
|
||||
|
||||
describe('read-only masterKey', () => {
|
||||
let loggerErrorSpy;
|
||||
let logger;
|
||||
|
||||
@@ -33,6 +33,15 @@ function enforceRoleSecurity(method, className, auth, config) {
|
||||
);
|
||||
}
|
||||
|
||||
// _Join tables are internal and must only be modified through relation operations
|
||||
if (className.startsWith('_Join:') && !auth.isMaster && !auth.isMaintenance) {
|
||||
throw createSanitizedError(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
`Clients aren't allowed to perform the ${method} operation on the ${className} collection.`,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
// readOnly masterKey is not allowed
|
||||
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
|
||||
throw createSanitizedError(
|
||||
|
||||
Reference in New Issue
Block a user