fix: protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) (#8073)

This commit is contained in:
Manuel
2022-06-30 12:26:39 +02:00
committed by GitHub
parent eb2952fff7
commit 309f64ced8
4 changed files with 132 additions and 24 deletions

View File

@@ -1066,6 +1066,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) { afterEach(async function (done) {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close(); client.close();

View File

@@ -127,7 +127,7 @@ const filterSensitiveData = (
aclGroup: any[], aclGroup: any[],
auth: any, auth: any,
operation: any, operation: any,
schema: SchemaController.SchemaController, schema: SchemaController.SchemaController | any,
className: string, className: string,
protectedFields: null | Array<any>, protectedFields: null | Array<any>,
object: any object: any
@@ -136,7 +136,8 @@ const filterSensitiveData = (
if (auth && auth.user) userId = auth.user.id; if (auth && auth.user) userId = auth.user.id;
// replace protectedFields when using pointer-permissions // replace protectedFields when using pointer-permissions
const perms = schema.getClassLevelPermissions(className); const perms =
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
if (perms) { if (perms) {
const isReadOperation = ['get', 'find'].indexOf(operation) > -1; const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
@@ -1533,14 +1534,17 @@ class DatabaseController {
} }
addProtectedFields( addProtectedFields(
schema: SchemaController.SchemaController, schema: SchemaController.SchemaController | any,
className: string, className: string,
query: any = {}, query: any = {},
aclGroup: any[] = [], aclGroup: any[] = [],
auth: any = {}, auth: any = {},
queryOptions: FullQueryOptions = {} queryOptions: FullQueryOptions = {}
): null | string[] { ): null | string[] {
const perms = schema.getClassLevelPermissions(className); const perms =
schema && schema.getClassLevelPermissions
? schema.getClassLevelPermissions(className)
: schema;
if (!perms) return null; if (!perms) return null;
const protectedFields = perms.protectedFields; const protectedFields = perms.protectedFields;
@@ -1806,8 +1810,10 @@ class DatabaseController {
} }
static _validateQuery: any => void; static _validateQuery: any => void;
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
} }
module.exports = DatabaseController; module.exports = DatabaseController;
// Expose validateQuery for tests // Expose validateQuery for tests
module.exports._validateQuery = validateQuery; module.exports._validateQuery = validateQuery;
module.exports.filterSensitiveData = filterSensitiveData;

View File

@@ -33,6 +33,9 @@ class ParseCloudCodePublisher {
if (request.original) { if (request.original) {
message.originalParseObject = request.original._toFullJSON(); message.originalParseObject = request.original._toFullJSON();
} }
if (request.classLevelPermissions) {
message.classLevelPermissions = request.classLevelPermissions;
}
this.parsePublisher.publish(type, JSON.stringify(message)); this.parsePublisher.publish(type, JSON.stringify(message));
} }
} }

View File

@@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
import SchemaController from '../Controllers/SchemaController'; import SchemaController from '../Controllers/SchemaController';
import _ from 'lodash'; import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers'; import {
runLiveQueryEventHandlers,
getTrigger,
runTrigger,
resolveError,
toJSONwithObjects,
} from '../triggers';
import { getAuthForSessionToken, Auth } from '../Auth'; import { getAuthForSessionToken, Auth } from '../Auth';
import { getCacheController } from '../Controllers'; import { getCacheController, getDatabaseController } from '../Controllers';
import LRU from 'lru-cache'; import LRU from 'lru-cache';
import UserRouter from '../Routers/UsersRouter'; import UserRouter from '../Routers/UsersRouter';
import DatabaseController from '../Controllers/DatabaseController';
class ParseLiveQueryServer { class ParseLiveQueryServer {
clients: Map; clients: Map;
@@ -185,14 +192,14 @@ class ParseLiveQueryServer {
if (res.object && typeof res.object.toJSON === 'function') { if (res.object && typeof res.object.toJSON === 'function') {
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className); deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
} }
if ( await this._filterSensitiveData(
(deletedParseObject.className === '_User' || classLevelPermissions,
deletedParseObject.className === '_Session') && res,
!client.hasMasterKey client,
) { requestId,
delete deletedParseObject.sessionToken; op,
delete deletedParseObject.authData; subscription.query
} );
client.pushDelete(requestId, deletedParseObject); client.pushDelete(requestId, deletedParseObject);
} catch (e) { } catch (e) {
const error = resolveError(e); const error = resolveError(e);
@@ -339,16 +346,14 @@ class ParseLiveQueryServer {
res.original.className || className res.original.className || className
); );
} }
if ( await this._filterSensitiveData(
(currentParseObject.className === '_User' || classLevelPermissions,
currentParseObject.className === '_Session') && res,
!client.hasMasterKey client,
) { requestId,
delete currentParseObject.sessionToken; op,
delete originalParseObject?.sessionToken; subscription.query
delete currentParseObject.authData; );
delete originalParseObject?.authData;
}
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1); const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
if (client[functionName]) { if (client[functionName]) {
client[functionName](requestId, currentParseObject, originalParseObject); client[functionName](requestId, currentParseObject, originalParseObject);
@@ -540,6 +545,54 @@ class ParseLiveQueryServer {
// return rolesQuery.find({useMasterKey:true}); // 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) { _getCLPOperation(query: any) {
return typeof query === 'object' && return typeof query === 'object' &&
Object.keys(query).length == 1 && Object.keys(query).length == 1 &&