Skip to content

Commit 10dd802

Browse files
committed
fix
1 parent 189f15e commit 10dd802

File tree

3 files changed

+123
-0
lines changed

3 files changed

+123
-0
lines changed

spec/helper.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ global.afterEachFn = async () => {
258258
if (!className.startsWith('_')) {
259259
return true;
260260
}
261+
if (className.startsWith('_Join:')) {
262+
return true;
263+
}
261264
return [
262265
'_User',
263266
'_Installation',

spec/rest.spec.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,117 @@ describe('rest update', () => {
953953
});
954954
});
955955

956+
describe('_Join table security', () => {
957+
let config;
958+
959+
beforeEach(() => {
960+
config = Config.get('test');
961+
});
962+
963+
it('cannot create object in _Join table without masterKey', () => {
964+
expect(() =>
965+
rest.create(config, auth.nobody(config), '_Join:users:_Role', {
966+
relatedId: 'someUserId',
967+
owningId: 'someRoleId',
968+
})
969+
).toThrowError(/Permission denied/);
970+
});
971+
972+
it('cannot find objects in _Join table without masterKey', async () => {
973+
await expectAsync(
974+
rest.find(config, auth.nobody(config), '_Join:users:_Role', {})
975+
).toBeRejectedWith(
976+
jasmine.objectContaining({
977+
code: Parse.Error.OPERATION_FORBIDDEN,
978+
})
979+
);
980+
});
981+
982+
it('cannot update object in _Join table without masterKey', () => {
983+
expect(() =>
984+
rest.update(config, auth.nobody(config), '_Join:users:_Role', { relatedId: 'someUserId' }, { owningId: 'newRoleId' })
985+
).toThrowError(/Permission denied/);
986+
});
987+
988+
it('cannot delete object in _Join table without masterKey', () => {
989+
expect(() =>
990+
rest.del(config, auth.nobody(config), '_Join:users:_Role', 'someObjectId')
991+
).toThrowError(/Permission denied/);
992+
});
993+
994+
it('cannot get object in _Join table without masterKey', async () => {
995+
await expectAsync(
996+
rest.get(config, auth.nobody(config), '_Join:users:_Role', 'someObjectId')
997+
).toBeRejectedWith(
998+
jasmine.objectContaining({
999+
code: Parse.Error.OPERATION_FORBIDDEN,
1000+
})
1001+
);
1002+
});
1003+
1004+
it('can find objects in _Join table with masterKey', async () => {
1005+
await expectAsync(
1006+
rest.find(config, auth.master(config), '_Join:users:_Role', {})
1007+
).toBeResolved();
1008+
});
1009+
1010+
it('can find objects in _Join table with maintenance key', async () => {
1011+
await expectAsync(
1012+
rest.find(config, auth.maintenance(config), '_Join:users:_Role', {})
1013+
).toBeResolved();
1014+
});
1015+
1016+
it('legitimate relation operations still work', async () => {
1017+
const role = new Parse.Role('admin', new Parse.ACL());
1018+
const user = await Parse.User.signUp('testuser', 'password123');
1019+
role.getUsers().add(user);
1020+
await role.save(null, { useMasterKey: true });
1021+
const result = await rest.find(config, auth.master(config), '_Join:users:_Role', {});
1022+
expect(result.results.length).toBe(1);
1023+
});
1024+
1025+
it('blocks _Join table access for any relation, not just _Role', () => {
1026+
expect(() =>
1027+
rest.create(config, auth.nobody(config), '_Join:viewers:ConfidentialDoc', {
1028+
relatedId: 'someUserId',
1029+
owningId: 'someDocId',
1030+
})
1031+
).toThrowError(/Permission denied/);
1032+
});
1033+
1034+
it('cannot escalate role via direct _Join table write', async () => {
1035+
const role = new Parse.Role('superadmin', new Parse.ACL());
1036+
await role.save(null, { useMasterKey: true });
1037+
const user = await Parse.User.signUp('attacker', 'password123');
1038+
const sessionToken = user.getSessionToken();
1039+
const userAuth = await auth.getAuthForSessionToken({
1040+
config,
1041+
sessionToken,
1042+
});
1043+
expect(() =>
1044+
rest.create(config, userAuth, '_Join:users:_Role', {
1045+
relatedId: user.id,
1046+
owningId: role.id,
1047+
})
1048+
).toThrowError(/Permission denied/);
1049+
});
1050+
1051+
it('cannot write to _Join table with read-only masterKey', () => {
1052+
expect(() =>
1053+
rest.create(config, auth.readOnly(config), '_Join:users:_Role', {
1054+
relatedId: 'someUserId',
1055+
owningId: 'someRoleId',
1056+
})
1057+
).toThrowError(/Permission denied/);
1058+
});
1059+
1060+
it('can read _Join table with read-only masterKey', async () => {
1061+
await expectAsync(
1062+
rest.find(config, auth.readOnly(config), '_Join:users:_Role', {})
1063+
).toBeResolved();
1064+
});
1065+
});
1066+
9561067
describe('read-only masterKey', () => {
9571068
let loggerErrorSpy;
9581069
let logger;

src/SharedRest.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ function enforceRoleSecurity(method, className, auth, config) {
3333
);
3434
}
3535

36+
// _Join tables are internal and must only be modified through relation operations
37+
if (className.startsWith('_Join:') && !auth.isMaster && !auth.isMaintenance) {
38+
throw createSanitizedError(
39+
Parse.Error.OPERATION_FORBIDDEN,
40+
`Clients aren't allowed to perform the ${method} operation on the ${className} collection.`,
41+
config
42+
);
43+
}
44+
3645
// readOnly masterKey is not allowed
3746
if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) {
3847
throw createSanitizedError(

0 commit comments

Comments
 (0)