Skip to content

Commit 309f64c

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) (#8073)
1 parent eb2952f commit 309f64c

File tree

4 files changed

+132
-24
lines changed

4 files changed

+132
-24
lines changed

spec/ParseLiveQuery.spec.js

+46
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,52 @@ describe('ParseLiveQuery', function () {
10661066
}
10671067
});
10681068

1069+
it('should strip out protected fields', async () => {
1070+
await reconfigureServer({
1071+
liveQuery: { classNames: ['Test'] },
1072+
startLiveQueryServer: true,
1073+
});
1074+
const obj1 = new Parse.Object('Test');
1075+
obj1.set('foo', 'foo');
1076+
obj1.set('bar', 'bar');
1077+
obj1.set('qux', 'qux');
1078+
await obj1.save();
1079+
const config = Config.get(Parse.applicationId);
1080+
const schemaController = await config.database.loadSchema();
1081+
await schemaController.updateClass(
1082+
'Test',
1083+
{},
1084+
{
1085+
get: { '*': true },
1086+
find: { '*': true },
1087+
update: { '*': true },
1088+
protectedFields: {
1089+
'*': ['foo'],
1090+
},
1091+
}
1092+
);
1093+
const object = await obj1.fetch();
1094+
expect(object.get('foo')).toBe(undefined);
1095+
expect(object.get('bar')).toBeDefined();
1096+
expect(object.get('qux')).toBeDefined();
1097+
1098+
const subscription = await new Parse.Query('Test').subscribe();
1099+
await Promise.all([
1100+
new Promise(resolve => {
1101+
subscription.on('update', (obj, original) => {
1102+
expect(obj.get('foo')).toBe(undefined);
1103+
expect(obj.get('bar')).toBeDefined();
1104+
expect(obj.get('qux')).toBeDefined();
1105+
expect(original.get('foo')).toBe(undefined);
1106+
expect(original.get('bar')).toBeDefined();
1107+
expect(original.get('qux')).toBeDefined();
1108+
resolve();
1109+
});
1110+
}),
1111+
obj1.save({ foo: 'abc' }),
1112+
]);
1113+
});
1114+
10691115
afterEach(async function (done) {
10701116
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
10711117
client.close();

src/Controllers/DatabaseController.js

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

138138
// replace protectedFields when using pointer-permissions
139-
const perms = schema.getClassLevelPermissions(className);
139+
const perms =
140+
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
140141
if (perms) {
141142
const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
142143

@@ -1533,14 +1534,17 @@ class DatabaseController {
15331534
}
15341535

15351536
addProtectedFields(
1536-
schema: SchemaController.SchemaController,
1537+
schema: SchemaController.SchemaController | any,
15371538
className: string,
15381539
query: any = {},
15391540
aclGroup: any[] = [],
15401541
auth: any = {},
15411542
queryOptions: FullQueryOptions = {}
15421543
): null | string[] {
1543-
const perms = schema.getClassLevelPermissions(className);
1544+
const perms =
1545+
schema && schema.getClassLevelPermissions
1546+
? schema.getClassLevelPermissions(className)
1547+
: schema;
15441548
if (!perms) return null;
15451549

15461550
const protectedFields = perms.protectedFields;
@@ -1806,8 +1810,10 @@ class DatabaseController {
18061810
}
18071811

18081812
static _validateQuery: any => void;
1813+
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
18091814
}
18101815

18111816
module.exports = DatabaseController;
18121817
// Expose validateQuery for tests
18131818
module.exports._validateQuery = validateQuery;
1819+
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

+73-20
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
1010
import SchemaController from '../Controllers/SchemaController';
1111
import _ from 'lodash';
1212
import { v4 as uuidv4 } from 'uuid';
13-
import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers';
13+
import {
14+
runLiveQueryEventHandlers,
15+
getTrigger,
16+
runTrigger,
17+
resolveError,
18+
toJSONwithObjects,
19+
} from '../triggers';
1420
import { getAuthForSessionToken, Auth } from '../Auth';
15-
import { getCacheController } from '../Controllers';
21+
import { getCacheController, getDatabaseController } from '../Controllers';
1622
import LRU from 'lru-cache';
1723
import UserRouter from '../Routers/UsersRouter';
24+
import DatabaseController from '../Controllers/DatabaseController';
1825

1926
class ParseLiveQueryServer {
2027
clients: Map;
@@ -185,14 +192,14 @@ class ParseLiveQueryServer {
185192
if (res.object && typeof res.object.toJSON === 'function') {
186193
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
187194
}
188-
if (
189-
(deletedParseObject.className === '_User' ||
190-
deletedParseObject.className === '_Session') &&
191-
!client.hasMasterKey
192-
) {
193-
delete deletedParseObject.sessionToken;
194-
delete deletedParseObject.authData;
195-
}
195+
await this._filterSensitiveData(
196+
classLevelPermissions,
197+
res,
198+
client,
199+
requestId,
200+
op,
201+
subscription.query
202+
);
196203
client.pushDelete(requestId, deletedParseObject);
197204
} catch (e) {
198205
const error = resolveError(e);
@@ -339,16 +346,14 @@ class ParseLiveQueryServer {
339346
res.original.className || className
340347
);
341348
}
342-
if (
343-
(currentParseObject.className === '_User' ||
344-
currentParseObject.className === '_Session') &&
345-
!client.hasMasterKey
346-
) {
347-
delete currentParseObject.sessionToken;
348-
delete originalParseObject?.sessionToken;
349-
delete currentParseObject.authData;
350-
delete originalParseObject?.authData;
351-
}
349+
await this._filterSensitiveData(
350+
classLevelPermissions,
351+
res,
352+
client,
353+
requestId,
354+
op,
355+
subscription.query
356+
);
352357
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
353358
if (client[functionName]) {
354359
client[functionName](requestId, currentParseObject, originalParseObject);
@@ -540,6 +545,54 @@ class ParseLiveQueryServer {
540545
// return rolesQuery.find({useMasterKey:true});
541546
}
542547

548+
async _filterSensitiveData(
549+
classLevelPermissions: ?any,
550+
res: any,
551+
client: any,
552+
requestId: number,
553+
op: string,
554+
query: any
555+
) {
556+
const subscriptionInfo = client.getSubscriptionInfo(requestId);
557+
const aclGroup = ['*'];
558+
let clientAuth;
559+
if (typeof subscriptionInfo !== 'undefined') {
560+
const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken);
561+
if (userId) {
562+
aclGroup.push(userId);
563+
}
564+
clientAuth = auth;
565+
}
566+
const filter = obj => {
567+
if (!obj) {
568+
return;
569+
}
570+
let protectedFields = classLevelPermissions?.protectedFields || [];
571+
if (!client.hasMasterKey && !Array.isArray(protectedFields)) {
572+
protectedFields = getDatabaseController(this.config).addProtectedFields(
573+
classLevelPermissions,
574+
res.object.className,
575+
query,
576+
aclGroup,
577+
clientAuth
578+
);
579+
}
580+
return DatabaseController.filterSensitiveData(
581+
client.hasMasterKey,
582+
aclGroup,
583+
clientAuth,
584+
op,
585+
classLevelPermissions,
586+
res.object.className,
587+
protectedFields,
588+
obj,
589+
query
590+
);
591+
};
592+
res.object = filter(res.object);
593+
res.original = filter(res.original);
594+
}
595+
543596
_getCLPOperation(query: any) {
544597
return typeof query === 'object' &&
545598
Object.keys(query).length == 1 &&

0 commit comments

Comments
 (0)