Skip to content

Commit 054f3e6

Browse files
authored
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](GHSA-crrq-vr9j-fxxh)) (#8074)
1 parent 6286d2e commit 054f3e6

File tree

4 files changed

+126
-25
lines changed

4 files changed

+126
-25
lines changed

spec/ParseLiveQuery.spec.js

+46
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,52 @@ describe('ParseLiveQuery', function () {
974974
}
975975
});
976976

977+
it('should strip out protected fields', async () => {
978+
await reconfigureServer({
979+
liveQuery: { classNames: ['Test'] },
980+
startLiveQueryServer: true,
981+
});
982+
const obj1 = new Parse.Object('Test');
983+
obj1.set('foo', 'foo');
984+
obj1.set('bar', 'bar');
985+
obj1.set('qux', 'qux');
986+
await obj1.save();
987+
const config = Config.get(Parse.applicationId);
988+
const schemaController = await config.database.loadSchema();
989+
await schemaController.updateClass(
990+
'Test',
991+
{},
992+
{
993+
get: { '*': true },
994+
find: { '*': true },
995+
update: { '*': true },
996+
protectedFields: {
997+
'*': ['foo'],
998+
},
999+
}
1000+
);
1001+
const object = await obj1.fetch();
1002+
expect(object.get('foo')).toBe(undefined);
1003+
expect(object.get('bar')).toBeDefined();
1004+
expect(object.get('qux')).toBeDefined();
1005+
1006+
const subscription = await new Parse.Query('Test').subscribe();
1007+
await Promise.all([
1008+
new Promise(resolve => {
1009+
subscription.on('update', (obj, original) => {
1010+
expect(obj.get('foo')).toBe(undefined);
1011+
expect(obj.get('bar')).toBeDefined();
1012+
expect(obj.get('qux')).toBeDefined();
1013+
expect(original.get('foo')).toBe(undefined);
1014+
expect(original.get('bar')).toBeDefined();
1015+
expect(original.get('qux')).toBeDefined();
1016+
resolve();
1017+
});
1018+
}),
1019+
obj1.save({ foo: 'abc' }),
1020+
]);
1021+
});
1022+
9771023
afterEach(async function (done) {
9781024
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
9791025
client.close();

src/Controllers/DatabaseController.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const filterSensitiveData = (
123123
aclGroup: any[],
124124
auth: any,
125125
operation: any,
126-
schema: SchemaController.SchemaController,
126+
schema: SchemaController.SchemaController | any,
127127
className: string,
128128
protectedFields: null | Array<any>,
129129
object: any
@@ -132,7 +132,8 @@ const filterSensitiveData = (
132132
if (auth && auth.user) userId = auth.user.id;
133133

134134
// replace protectedFields when using pointer-permissions
135-
const perms = schema.getClassLevelPermissions(className);
135+
const perms =
136+
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
136137
if (perms) {
137138
const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
138139

@@ -1430,14 +1431,17 @@ class DatabaseController {
14301431
}
14311432

14321433
addProtectedFields(
1433-
schema: SchemaController.SchemaController,
1434+
schema: SchemaController.SchemaController | any,
14341435
className: string,
14351436
query: any = {},
14361437
aclGroup: any[] = [],
14371438
auth: any = {},
14381439
queryOptions: FullQueryOptions = {}
14391440
): null | string[] {
1440-
const perms = schema.getClassLevelPermissions(className);
1441+
const perms =
1442+
schema && schema.getClassLevelPermissions
1443+
? schema.getClassLevelPermissions(className)
1444+
: schema;
14411445
if (!perms) return null;
14421446

14431447
const protectedFields = perms.protectedFields;
@@ -1741,8 +1745,10 @@ class DatabaseController {
17411745
}
17421746

17431747
static _validateQuery: any => void;
1748+
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
17441749
}
17451750

17461751
module.exports = DatabaseController;
17471752
// Expose validateQuery for tests
17481753
module.exports._validateQuery = validateQuery;
1754+
module.exports.filterSensitiveData = filterSensitiveData;

src/LiveQuery/ParseCloudCodePublisher.js

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class ParseCloudCodePublisher {
3333
if (request.original) {
3434
message.originalParseObject = request.original._toFullJSON();
3535
}
36+
if (request.classLevelPermissions) {
37+
message.classLevelPermissions = request.classLevelPermissions;
38+
}
3639
this.parsePublisher.publish(type, JSON.stringify(message));
3740
}
3841
}

src/LiveQuery/ParseLiveQueryServer.js

+67-21
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
maybeRunAfterEventTrigger,
1818
} from '../triggers';
1919
import { getAuthForSessionToken, Auth } from '../Auth';
20-
import { getCacheController } from '../Controllers';
20+
import { getCacheController, getDatabaseController } from '../Controllers';
2121
import LRU from 'lru-cache';
2222
import UserRouter from '../Routers/UsersRouter';
23+
import DatabaseController from '../Controllers/DatabaseController';
2324

2425
class ParseLiveQueryServer {
2526
clients: Map;
@@ -171,22 +172,22 @@ class ParseLiveQueryServer {
171172
};
172173
return maybeRunAfterEventTrigger('afterEvent', className, res);
173174
})
174-
.then(() => {
175+
.then(async () => {
175176
if (!res.sendEvent) {
176177
return;
177178
}
178179
if (res.object && typeof res.object.toJSON === 'function') {
179180
deletedParseObject = res.object.toJSON();
180181
deletedParseObject.className = className;
181182
}
182-
if (
183-
(deletedParseObject.className === '_User' ||
184-
deletedParseObject.className === '_Session') &&
185-
!client.hasMasterKey
186-
) {
187-
delete deletedParseObject.sessionToken;
188-
delete deletedParseObject.authData;
189-
}
183+
await this._filterSensitiveData(
184+
classLevelPermissions,
185+
res,
186+
client,
187+
requestId,
188+
op,
189+
subscription.query
190+
);
190191
client.pushDelete(requestId, deletedParseObject);
191192
})
192193
.catch(error => {
@@ -310,7 +311,7 @@ class ParseLiveQueryServer {
310311
return maybeRunAfterEventTrigger('afterEvent', className, res);
311312
})
312313
.then(
313-
() => {
314+
async () => {
314315
if (!res.sendEvent) {
315316
return;
316317
}
@@ -323,16 +324,14 @@ class ParseLiveQueryServer {
323324
originalParseObject = res.original.toJSON();
324325
originalParseObject.className = res.original.className || className;
325326
}
326-
if (
327-
(currentParseObject.className === '_User' ||
328-
currentParseObject.className === '_Session') &&
329-
!client.hasMasterKey
330-
) {
331-
delete currentParseObject.sessionToken;
332-
delete originalParseObject?.sessionToken;
333-
delete currentParseObject.authData;
334-
delete originalParseObject?.authData;
335-
}
327+
await this._filterSensitiveData(
328+
classLevelPermissions,
329+
res,
330+
client,
331+
requestId,
332+
op,
333+
subscription.query
334+
);
336335
const functionName =
337336
'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
338337
if (client[functionName]) {
@@ -532,6 +531,53 @@ class ParseLiveQueryServer {
532531
// return rolesQuery.find({useMasterKey:true});
533532
}
534533

534+
async _filterSensitiveData(
535+
classLevelPermissions: ?any,
536+
res: any,
537+
client: any,
538+
requestId: number,
539+
op: string,
540+
query: any
541+
) {
542+
const subscriptionInfo = client.getSubscriptionInfo(requestId);
543+
const aclGroup = ['*'];
544+
let clientAuth;
545+
if (typeof subscriptionInfo !== 'undefined') {
546+
const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
547+
if (userId) {
548+
aclGroup.push(userId);
549+
}
550+
clientAuth = auth;
551+
}
552+
const filter = obj => {
553+
if (!obj) {
554+
return;
555+
}
556+
let protectedFields = classLevelPermissions?.protectedFields || [];
557+
if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
558+
protectedFields = getDatabaseController(this.config).addProtectedFields(
559+
classLevelPermissions,
560+
res.object.className,
561+
query,
562+
aclGroup,
563+
clientAuth
564+
);
565+
}
566+
return DatabaseController.filterSensitiveData(
567+
client.hasMasterKey,
568+
aclGroup,
569+
clientAuth,
570+
op,
571+
classLevelPermissions,
572+
res.object.className,
573+
protectedFields,
574+
obj
575+
);
576+
};
577+
res.object = filter(res.object);
578+
res.original = filter(res.original);
579+
}
580+
535581
_getCLPOperation(query: any) {
536582
return typeof query === 'object' &&
537583
Object.keys(query).length == 1 &&

0 commit comments

Comments
 (0)