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)) (https://github.com/parse-community/parse-server/pull/8074) (#8073)
This commit is contained in:
@@ -1066,6 +1066,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();
|
||||||
|
|||||||
@@ -127,7 +127,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
|
||||||
@@ -136,7 +136,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;
|
||||||
|
|
||||||
@@ -1533,14 +1534,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;
|
||||||
@@ -1806,8 +1810,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;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
|
|||||||
import SchemaController from '../Controllers/SchemaController';
|
import SchemaController from '../Controllers/SchemaController';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers';
|
import {
|
||||||
|
runLiveQueryEventHandlers,
|
||||||
|
getTrigger,
|
||||||
|
runTrigger,
|
||||||
|
resolveError,
|
||||||
|
toJSONwithObjects,
|
||||||
|
} 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;
|
||||||
@@ -185,14 +192,14 @@ class ParseLiveQueryServer {
|
|||||||
if (res.object && typeof res.object.toJSON === 'function') {
|
if (res.object && typeof res.object.toJSON === 'function') {
|
||||||
deletedParseObject = toJSONwithObjects(res.object, res.object.className || className);
|
deletedParseObject = toJSONwithObjects(res.object, res.object.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 (e) {
|
} catch (e) {
|
||||||
const error = resolveError(e);
|
const error = resolveError(e);
|
||||||
@@ -339,16 +346,14 @@ class ParseLiveQueryServer {
|
|||||||
res.original.className || 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 = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
|
const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
|
||||||
if (client[functionName]) {
|
if (client[functionName]) {
|
||||||
client[functionName](requestId, currentParseObject, originalParseObject);
|
client[functionName](requestId, currentParseObject, originalParseObject);
|
||||||
@@ -540,6 +545,54 @@ 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,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
};
|
||||||
|
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 &&
|
||||||
|
|||||||
Reference in New Issue
Block a user