From 9fd4516cde5c742f9f29dd05468b4a43a85639a6 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 30 Jun 2022 13:01:40 +0200 Subject: [PATCH] fix: protected fields exposed via LiveQuery (GHSA-crrq-vr9j-fxxh) [skip release] (#8076) --- spec/ParseLiveQuery.spec.js | 46 +++++++++++++ src/Controllers/DatabaseController.js | 14 ++-- src/LiveQuery/ParseCloudCodePublisher.js | 3 + src/LiveQuery/ParseLiveQueryServer.js | 85 ++++++++++++++++++------ 4 files changed, 125 insertions(+), 23 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index dd0610f966..7f3794fa62 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -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(); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 3e69b1f5eb..8e99b29216 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -127,7 +127,7 @@ const filterSensitiveData = ( aclGroup: any[], auth: any, operation: any, - schema: SchemaController.SchemaController, + schema: SchemaController.SchemaController | any, className: string, protectedFields: null | Array, object: any @@ -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; @@ -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; @@ -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; diff --git a/src/LiveQuery/ParseCloudCodePublisher.js b/src/LiveQuery/ParseCloudCodePublisher.js index b84b14ef31..3ecd740e12 100644 --- a/src/LiveQuery/ParseCloudCodePublisher.js +++ b/src/LiveQuery/ParseCloudCodePublisher.js @@ -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)); } } diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 88e98ce233..02c6ad30a1 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -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; @@ -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); @@ -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); @@ -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 &&