Skip to content

Commit

Permalink
fix: protected fields exposed via LiveQuery; this removes protected f…
Browse files Browse the repository at this point in the history
…ields 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](GHSA-crrq-vr9j-fxxh)) (parse-community#8074)
  • Loading branch information
mtrezza authored Jun 30, 2022
1 parent 6286d2e commit 054f3e6
Showing 4 changed files with 126 additions and 25 deletions.
46 changes: 46 additions & 0 deletions spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
@@ -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();
14 changes: 10 additions & 4 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions src/LiveQuery/ParseCloudCodePublisher.js
Original file line number Diff line number Diff line change
@@ -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));
}
}
88 changes: 67 additions & 21 deletions src/LiveQuery/ParseLiveQueryServer.js
Original file line number Diff line number Diff line change
@@ -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,22 +172,22 @@ class ParseLiveQueryServer {
};
return maybeRunAfterEventTrigger('afterEvent', className, res);
})
.then(() => {
.then(async () => {
if (!res.sendEvent) {
return;
}
if (res.object && typeof res.object.toJSON === 'function') {
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 &&

0 comments on commit 054f3e6

Please sign in to comment.