Skip to content

Commit

Permalink
fix: protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [sk…
Browse files Browse the repository at this point in the history
…ip release] (parse-community#8076)
  • Loading branch information
mtrezza committed Jun 30, 2022
1 parent e8eb546 commit 9fd4516
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 23 deletions.
46 changes: 46 additions & 0 deletions spec/ParseLiveQuery.spec.js
Expand Up @@ -1110,6 +1110,52 @@ describe('ParseLiveQuery', function () {
}
});

it('should strip out protected fields', async () => {
await reconfigureServer({
liveQuery: { classNames: ['Test'] },
startLiveQueryServer: true,
});
const obj1 = new Parse.Object('Test');
obj1.set('foo', 'foo');
obj1.set('bar', 'bar');
obj1.set('qux', 'qux');
await obj1.save();
const config = Config.get(Parse.applicationId);
const schemaController = await config.database.loadSchema();
await schemaController.updateClass(
'Test',
{},
{
get: { '*': true },
find: { '*': true },
update: { '*': true },
protectedFields: {
'*': ['foo'],
},
}
);
const object = await obj1.fetch();
expect(object.get('foo')).toBe(undefined);
expect(object.get('bar')).toBeDefined();
expect(object.get('qux')).toBeDefined();

const subscription = await new Parse.Query('Test').subscribe();
await Promise.all([
new Promise(resolve => {
subscription.on('update', (obj, original) => {
expect(obj.get('foo')).toBe(undefined);
expect(obj.get('bar')).toBeDefined();
expect(obj.get('qux')).toBeDefined();
expect(original.get('foo')).toBe(undefined);
expect(original.get('bar')).toBeDefined();
expect(original.get('qux')).toBeDefined();
resolve();
});
}),
obj1.save({ foo: 'abc' }),
]);
});

afterEach(async function (done) {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close();
Expand Down
14 changes: 10 additions & 4 deletions src/Controllers/DatabaseController.js
Expand Up @@ -127,7 +127,7 @@ const filterSensitiveData = (
aclGroup: any[],
auth: any,
operation: any,
schema: SchemaController.SchemaController,
schema: SchemaController.SchemaController | any,
className: string,
protectedFields: null | Array<any>,
object: any
Expand All @@ -136,7 +136,8 @@ const filterSensitiveData = (
if (auth && auth.user) userId = auth.user.id;

// replace protectedFields when using pointer-permissions
const perms = schema.getClassLevelPermissions(className);
const perms =
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
if (perms) {
const isReadOperation = ['get', 'find'].indexOf(operation) > -1;

Expand Down Expand Up @@ -1533,14 +1534,17 @@ class DatabaseController {
}

addProtectedFields(
schema: SchemaController.SchemaController,
schema: SchemaController.SchemaController | any,
className: string,
query: any = {},
aclGroup: any[] = [],
auth: any = {},
queryOptions: FullQueryOptions = {}
): null | string[] {
const perms = schema.getClassLevelPermissions(className);
const perms =
schema && schema.getClassLevelPermissions
? schema.getClassLevelPermissions(className)
: schema;
if (!perms) return null;

const protectedFields = perms.protectedFields;
Expand Down Expand Up @@ -1806,8 +1810,10 @@ class DatabaseController {
}

static _validateQuery: any => void;
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
}

module.exports = DatabaseController;
// Expose validateQuery for tests
module.exports._validateQuery = validateQuery;
module.exports.filterSensitiveData = filterSensitiveData;
3 changes: 3 additions & 0 deletions src/LiveQuery/ParseCloudCodePublisher.js
Expand Up @@ -40,6 +40,9 @@ class ParseCloudCodePublisher {
if (request.original) {
message.originalParseObject = request.original._toFullJSON();
}
if (request.classLevelPermissions) {
message.classLevelPermissions = request.classLevelPermissions;
}
this.parsePublisher.publish(type, JSON.stringify(message));
}
}
Expand Down
85 changes: 66 additions & 19 deletions src/LiveQuery/ParseLiveQueryServer.js
Expand Up @@ -18,9 +18,10 @@ import {
toJSONwithObjects,
} from '../triggers';
import { getAuthForSessionToken, Auth } from '../Auth';
import { getCacheController } from '../Controllers';
import { getCacheController, getDatabaseController } from '../Controllers';
import LRU from 'lru-cache';
import UserRouter from '../Routers/UsersRouter';
import DatabaseController from '../Controllers/DatabaseController';

class ParseLiveQueryServer {
clients: Map;
Expand Down Expand Up @@ -196,14 +197,14 @@ class ParseLiveQueryServer {
if (res.object && typeof res.object.toJSON === 'function') {
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
}
if (
(deletedParseObject.className === '_User' ||
deletedParseObject.className === '_Session') &&
!client.hasMasterKey
) {
delete deletedParseObject.sessionToken;
delete deletedParseObject.authData;
}
await this._filterSensitiveData(
classLevelPermissions,
res,
client,
requestId,
op,
subscription.query
);
client.pushDelete(requestId, deletedParseObject);
} catch (e) {
const error = resolveError(e);
Expand Down Expand Up @@ -350,16 +351,14 @@ class ParseLiveQueryServer {
res.original.className || className
);
}
if (
(currentParseObject.className === '_User' ||
currentParseObject.className === '_Session') &&
!client.hasMasterKey
) {
delete currentParseObject.sessionToken;
delete originalParseObject?.sessionToken;
delete currentParseObject.authData;
delete originalParseObject?.authData;
}
await this._filterSensitiveData(
classLevelPermissions,
res,
client,
requestId,
op,
subscription.query
);
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
if (client[functionName]) {
client[functionName](requestId, currentParseObject, originalParseObject);
Expand Down Expand Up @@ -577,6 +576,54 @@ class ParseLiveQueryServer {
// return rolesQuery.find({useMasterKey:true});
}

async _filterSensitiveData(
classLevelPermissions: ?any,
res: any,
client: any,
requestId: number,
op: string,
query: any
) {
const subscriptionInfo = client.getSubscriptionInfo(requestId);
const aclGroup = ['*'];
let clientAuth;
if (typeof subscriptionInfo !== 'undefined') {
const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
if (userId) {
aclGroup.push(userId);
}
clientAuth = auth;
}
const filter = obj => {
if (!obj) {
return;
}
let protectedFields = classLevelPermissions?.protectedFields || [];
if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
protectedFields = getDatabaseController(this.config).addProtectedFields(
classLevelPermissions,
res.object.className,
query,
aclGroup,
clientAuth
);
}
return DatabaseController.filterSensitiveData(
client.hasMasterKey,
aclGroup,
clientAuth,
op,
classLevelPermissions,
res.object.className,
protectedFields,
obj,
query
);
};
res.object = filter(res.object);
res.original = filter(res.original);
}

_getCLPOperation(query: any) {
return typeof query === 'object' &&
Object.keys(query).length == 1 &&
Expand Down

0 comments on commit 9fd4516

Please sign in to comment.