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)) (#8074)

This commit is contained in:
Manuel
2022-06-30 12:24:34 +02:00
committed by GitHub
parent 6286d2e34f
commit 054f3e6ab0
4 changed files with 126 additions and 25 deletions

View File

@@ -974,6 +974,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

@@ -123,7 +123,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
@@ -132,7 +132,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;
@@ -1430,14 +1431,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;
@@ -1741,8 +1745,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

@@ -17,9 +17,10 @@ import {
maybeRunAfterEventTrigger, maybeRunAfterEventTrigger,
} from '../triggers'; } 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;
@@ -171,7 +172,7 @@ class ParseLiveQueryServer {
}; };
return maybeRunAfterEventTrigger('afterEvent', className, res); return maybeRunAfterEventTrigger('afterEvent', className, res);
}) })
.then(() => { .then(async () => {
if (!res.sendEvent) { if (!res.sendEvent) {
return; return;
} }
@@ -179,14 +180,14 @@ class ParseLiveQueryServer {
deletedParseObject = res.object.toJSON(); deletedParseObject = res.object.toJSON();
deletedParseObject.className = className; deletedParseObject.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(error => { .catch(error => {
@@ -310,7 +311,7 @@ class ParseLiveQueryServer {
return maybeRunAfterEventTrigger('afterEvent', className, res); return maybeRunAfterEventTrigger('afterEvent', className, res);
}) })
.then( .then(
() => { async () => {
if (!res.sendEvent) { if (!res.sendEvent) {
return; return;
} }
@@ -323,16 +324,14 @@ class ParseLiveQueryServer {
originalParseObject = res.original.toJSON(); originalParseObject = res.original.toJSON();
originalParseObject.className = res.original.className || className; originalParseObject.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 = const functionName =
'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1); 'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
if (client[functionName]) { if (client[functionName]) {
@@ -532,6 +531,53 @@ 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
);
};
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 &&