mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Bypass of class-level permissions in LiveQuery ([GHSA-7ch5-98q2-7289](https://github.com/parse-community/parse-server/security/advisories/GHSA-7ch5-98q2-7289)) (#10134)
This commit is contained in:
@@ -1308,4 +1308,88 @@ describe('ParseLiveQuery', function () {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(createSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('class level permissions', () => {
|
||||
async function setPermissionsOnClass(className, permissions, doPut) {
|
||||
const method = doPut ? 'PUT' : 'POST';
|
||||
const response = await fetch(Parse.serverURL + '/schemas/' + className, {
|
||||
method,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
classLevelPermissions: permissions,
|
||||
}),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (body.error) {
|
||||
throw body;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
it('delivers LiveQuery event to authenticated client when CLP allows find', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['SecureChat'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('admin');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
|
||||
await setPermissionsOnClass('SecureChat', {
|
||||
create: { '*': true },
|
||||
find: { [user.id]: true },
|
||||
});
|
||||
|
||||
// Subscribe as the authorized user
|
||||
const query = new Parse.Query('SecureChat');
|
||||
const subscription = await query.subscribe(user.getSessionToken());
|
||||
|
||||
const spy = jasmine.createSpy('create');
|
||||
subscription.on('create', spy);
|
||||
|
||||
const obj = new Parse.Object('SecureChat');
|
||||
obj.set('secret', 'data');
|
||||
await obj.save(null, { useMasterKey: true });
|
||||
|
||||
await sleep(500);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects LiveQuery subscription when CLP denies find at subscription time', async () => {
|
||||
await reconfigureServer({
|
||||
liveQuery: {
|
||||
classNames: ['SecureChat'],
|
||||
},
|
||||
startLiveQueryServer: true,
|
||||
verbose: false,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('admin');
|
||||
user.setPassword('password');
|
||||
await user.signUp();
|
||||
|
||||
await setPermissionsOnClass('SecureChat', {
|
||||
create: { '*': true },
|
||||
find: { [user.id]: true },
|
||||
});
|
||||
|
||||
// Log out so subscription is unauthenticated
|
||||
await Parse.User.logOut();
|
||||
|
||||
const query = new Parse.Query('SecureChat');
|
||||
await expectAsync(query.subscribe()).toBeRejected();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1575,11 +1575,8 @@ describe('ParseLiveQueryServer', function () {
|
||||
});
|
||||
|
||||
describe('class level permissions', () => {
|
||||
it('matches CLP when find is closed', done => {
|
||||
it('rejects CLP when find is closed', async () => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
const client = {
|
||||
sessionToken: 'sessionToken',
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
@@ -1588,27 +1585,19 @@ describe('ParseLiveQueryServer', function () {
|
||||
};
|
||||
const requestId = 0;
|
||||
|
||||
parseLiveQueryServer
|
||||
._matchesCLP(
|
||||
{
|
||||
find: {},
|
||||
},
|
||||
await expectAsync(
|
||||
parseLiveQueryServer._matchesCLP(
|
||||
{ find: {} },
|
||||
{ className: 'Yolo' },
|
||||
client,
|
||||
requestId,
|
||||
'find'
|
||||
)
|
||||
.then(isMatched => {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
).toBeRejected();
|
||||
});
|
||||
|
||||
it('matches CLP when find is open', done => {
|
||||
it('resolves CLP when find is open', async () => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
const client = {
|
||||
sessionToken: 'sessionToken',
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
@@ -1617,27 +1606,19 @@ describe('ParseLiveQueryServer', function () {
|
||||
};
|
||||
const requestId = 0;
|
||||
|
||||
parseLiveQueryServer
|
||||
._matchesCLP(
|
||||
{
|
||||
find: { '*': true },
|
||||
},
|
||||
await expectAsync(
|
||||
parseLiveQueryServer._matchesCLP(
|
||||
{ find: { '*': true } },
|
||||
{ className: 'Yolo' },
|
||||
client,
|
||||
requestId,
|
||||
'find'
|
||||
)
|
||||
.then(isMatched => {
|
||||
expect(isMatched).toBe(true);
|
||||
done();
|
||||
});
|
||||
).toBeResolved();
|
||||
});
|
||||
|
||||
it('matches CLP when find is restricted to userIds', done => {
|
||||
it('rejects CLP when find is restricted to userIds', async () => {
|
||||
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setReadAccess(testUserId, true);
|
||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
||||
const client = {
|
||||
sessionToken: 'sessionToken',
|
||||
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
|
||||
@@ -1646,20 +1627,15 @@ describe('ParseLiveQueryServer', function () {
|
||||
};
|
||||
const requestId = 0;
|
||||
|
||||
parseLiveQueryServer
|
||||
._matchesCLP(
|
||||
{
|
||||
find: { userId: true },
|
||||
},
|
||||
await expectAsync(
|
||||
parseLiveQueryServer._matchesCLP(
|
||||
{ find: { userId: true } },
|
||||
{ className: 'Yolo' },
|
||||
client,
|
||||
requestId,
|
||||
'find'
|
||||
)
|
||||
.then(isMatched => {
|
||||
expect(isMatched).toBe(false);
|
||||
done();
|
||||
});
|
||||
).toBeRejected();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '../triggers';
|
||||
import { getAuthForSessionToken, Auth } from '../Auth';
|
||||
import { getCacheController, getDatabaseController } from '../Controllers';
|
||||
import Config from '../Config';
|
||||
import { LRUCache as LRU } from 'lru-cache';
|
||||
import UserRouter from '../Routers/UsersRouter';
|
||||
import DatabaseController from '../Controllers/DatabaseController';
|
||||
@@ -590,39 +591,20 @@ class ParseLiveQueryServer {
|
||||
requestId?: number,
|
||||
op?: string
|
||||
): Promise<any> {
|
||||
// try to match on user first, less expensive than with roles
|
||||
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
const aclGroup = ['*'];
|
||||
let userId;
|
||||
if (typeof subscriptionInfo !== 'undefined') {
|
||||
const { userId } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
|
||||
if (userId) {
|
||||
aclGroup.push(userId);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await SchemaController.validatePermission(
|
||||
classLevelPermissions,
|
||||
object.className,
|
||||
aclGroup,
|
||||
op
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.verbose(`Failed matching CLP for ${object.id} ${userId} ${e}`);
|
||||
return false;
|
||||
}
|
||||
// TODO: handle roles permissions
|
||||
// Object.keys(classLevelPermissions).forEach((key) => {
|
||||
// const perm = classLevelPermissions[key];
|
||||
// Object.keys(perm).forEach((key) => {
|
||||
// if (key.indexOf('role'))
|
||||
// });
|
||||
// })
|
||||
// // it's rejected here, check the roles
|
||||
// var rolesQuery = new Parse.Query(Parse.Role);
|
||||
// rolesQuery.equalTo("users", user);
|
||||
// return rolesQuery.find({useMasterKey:true});
|
||||
await SchemaController.validatePermission(
|
||||
classLevelPermissions,
|
||||
object.className,
|
||||
aclGroup,
|
||||
op
|
||||
);
|
||||
}
|
||||
|
||||
async _filterSensitiveData(
|
||||
@@ -907,6 +889,33 @@ class ParseLiveQueryServer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check CLP for subscribe operation
|
||||
const appConfig = Config.get(this.config.appId);
|
||||
const schemaController = await appConfig.database.loadSchema();
|
||||
const classLevelPermissions = schemaController.getClassLevelPermissions(className);
|
||||
const op = this._getCLPOperation(request.query);
|
||||
const aclGroup = ['*'];
|
||||
if (!authCalled) {
|
||||
const auth = await this.getAuthFromClient(
|
||||
client,
|
||||
request.requestId,
|
||||
request.sessionToken
|
||||
);
|
||||
authCalled = true;
|
||||
if (auth && auth.user) {
|
||||
request.user = auth.user;
|
||||
aclGroup.push(auth.user.id);
|
||||
}
|
||||
} else if (request.user) {
|
||||
aclGroup.push(request.user.id);
|
||||
}
|
||||
await SchemaController.validatePermission(
|
||||
classLevelPermissions,
|
||||
className,
|
||||
aclGroup,
|
||||
op
|
||||
);
|
||||
|
||||
// Get subscription from subscriptions, create one if necessary
|
||||
const subscriptionHash = queryHash(request.query);
|
||||
// Add className to subscriptions if necessary
|
||||
|
||||
Reference in New Issue
Block a user