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) {
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
client.close();

View File

@@ -123,7 +123,7 @@ const filterSensitiveData = (
aclGroup: any[],
auth: any,
operation: any,
schema: SchemaController.SchemaController,
schema: SchemaController.SchemaController | any,
className: string,
protectedFields: null | Array<any>,
object: any
@@ -132,7 +132,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;
@@ -1430,14 +1431,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;
@@ -1741,8 +1745,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;

View File

@@ -33,6 +33,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));
}
}

View File

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