Skip to content

Commit b1a1140

Browse files
authored
Remove hidden properties from aggregate responses (parse-community#4351)
* Remove hidden properties from aggregrate responses * transform results from mongo & postgres * Adjust ordering to comply with tests
1 parent 5fc0845 commit b1a1140

File tree

5 files changed

+144
-84
lines changed

5 files changed

+144
-84
lines changed

spec/ParseQuery.Aggregate.spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,4 +409,49 @@ describe('Parse.Query Aggregate testing', () => {
409409
done();
410410
}).catch(done.fail);
411411
});
412+
413+
it('does not return sensitive hidden properties', (done) => {
414+
const options = Object.assign({}, masterKeyOptions, {
415+
body: {
416+
match: {
417+
score: {
418+
$gt: 5
419+
}
420+
},
421+
}
422+
});
423+
424+
const username = 'leaky_user';
425+
const score = 10;
426+
427+
const user = new Parse.User();
428+
user.setUsername(username);
429+
user.setPassword('password');
430+
user.set('score', score);
431+
user.signUp().then(function() {
432+
return rp.get(Parse.serverURL + '/aggregate/_User', options);
433+
}).then(function(resp) {
434+
expect(resp.results.length).toBe(1);
435+
const result = resp.results[0];
436+
437+
// verify server-side keys are not present...
438+
expect(result._hashed_password).toBe(undefined);
439+
expect(result._wperm).toBe(undefined);
440+
expect(result._rperm).toBe(undefined);
441+
expect(result._acl).toBe(undefined);
442+
expect(result._created_at).toBe(undefined);
443+
expect(result._updated_at).toBe(undefined);
444+
445+
// verify createdAt, updatedAt and others are present
446+
expect(result.createdAt).not.toBe(undefined);
447+
expect(result.updatedAt).not.toBe(undefined);
448+
expect(result.objectId).not.toBe(undefined);
449+
expect(result.username).toBe(username);
450+
expect(result.score).toBe(score);
451+
452+
done();
453+
}).catch(function(err) {
454+
fail(err);
455+
});
456+
});
412457
});

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,11 @@ export class MongoStorageAdapter {
409409
distinct(className, schema, query, fieldName) {
410410
schema = convertParseSchemaToMongoSchema(schema);
411411
return this._adaptiveCollection(className)
412-
.then(collection => collection.distinct(fieldName, transformWhere(className, query, schema)));
412+
.then(collection => collection.distinct(fieldName, transformWhere(className, query, schema)))
413+
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)));
413414
}
414415

415-
aggregate(className, pipeline, readPreference) {
416+
aggregate(className, schema, pipeline, readPreference) {
416417
readPreference = this._parseReadPreference(readPreference);
417418
return this._adaptiveCollection(className)
418419
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
@@ -424,7 +425,8 @@ export class MongoStorageAdapter {
424425
}
425426
});
426427
return results;
427-
});
428+
})
429+
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)));
428430
}
429431

430432
_parseReadPreference(readPreference) {

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

Lines changed: 84 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,79 +1261,83 @@ export class PostgresStorageAdapter {
12611261
}
12621262
return Promise.reject(err);
12631263
})
1264-
.then(results => results.map(object => {
1265-
Object.keys(schema.fields).forEach(fieldName => {
1266-
if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) {
1267-
object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
1268-
}
1269-
if (schema.fields[fieldName].type === 'Relation') {
1270-
object[fieldName] = {
1271-
__type: "Relation",
1272-
className: schema.fields[fieldName].targetClass
1273-
}
1274-
}
1275-
if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') {
1276-
object[fieldName] = {
1277-
__type: "GeoPoint",
1278-
latitude: object[fieldName].y,
1279-
longitude: object[fieldName].x
1280-
}
1281-
}
1282-
if (object[fieldName] && schema.fields[fieldName].type === 'Polygon') {
1283-
let coords = object[fieldName];
1284-
coords = coords.substr(2, coords.length - 4).split('),(');
1285-
coords = coords.map((point) => {
1286-
return [
1287-
parseFloat(point.split(',')[1]),
1288-
parseFloat(point.split(',')[0])
1289-
];
1290-
});
1291-
object[fieldName] = {
1292-
__type: "Polygon",
1293-
coordinates: coords
1294-
}
1295-
}
1296-
if (object[fieldName] && schema.fields[fieldName].type === 'File') {
1297-
object[fieldName] = {
1298-
__type: 'File',
1299-
name: object[fieldName]
1300-
}
1301-
}
1302-
});
1303-
//TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field.
1304-
if (object.createdAt) {
1305-
object.createdAt = object.createdAt.toISOString();
1306-
}
1307-
if (object.updatedAt) {
1308-
object.updatedAt = object.updatedAt.toISOString();
1309-
}
1310-
if (object.expiresAt) {
1311-
object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() };
1312-
}
1313-
if (object._email_verify_token_expires_at) {
1314-
object._email_verify_token_expires_at = { __type: 'Date', iso: object._email_verify_token_expires_at.toISOString() };
1264+
.then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema)));
1265+
}
1266+
1267+
// Converts from a postgres-format object to a REST-format object.
1268+
// Does not strip out anything based on a lack of authentication.
1269+
postgresObjectToParseObject(className, object, schema) {
1270+
Object.keys(schema.fields).forEach(fieldName => {
1271+
if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) {
1272+
object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
1273+
}
1274+
if (schema.fields[fieldName].type === 'Relation') {
1275+
object[fieldName] = {
1276+
__type: "Relation",
1277+
className: schema.fields[fieldName].targetClass
13151278
}
1316-
if (object._account_lockout_expires_at) {
1317-
object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() };
1279+
}
1280+
if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') {
1281+
object[fieldName] = {
1282+
__type: "GeoPoint",
1283+
latitude: object[fieldName].y,
1284+
longitude: object[fieldName].x
13181285
}
1319-
if (object._perishable_token_expires_at) {
1320-
object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() };
1286+
}
1287+
if (object[fieldName] && schema.fields[fieldName].type === 'Polygon') {
1288+
let coords = object[fieldName];
1289+
coords = coords.substr(2, coords.length - 4).split('),(');
1290+
coords = coords.map((point) => {
1291+
return [
1292+
parseFloat(point.split(',')[1]),
1293+
parseFloat(point.split(',')[0])
1294+
];
1295+
});
1296+
object[fieldName] = {
1297+
__type: "Polygon",
1298+
coordinates: coords
13211299
}
1322-
if (object._password_changed_at) {
1323-
object._password_changed_at = { __type: 'Date', iso: object._password_changed_at.toISOString() };
1300+
}
1301+
if (object[fieldName] && schema.fields[fieldName].type === 'File') {
1302+
object[fieldName] = {
1303+
__type: 'File',
1304+
name: object[fieldName]
13241305
}
1306+
}
1307+
});
1308+
//TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field.
1309+
if (object.createdAt) {
1310+
object.createdAt = object.createdAt.toISOString();
1311+
}
1312+
if (object.updatedAt) {
1313+
object.updatedAt = object.updatedAt.toISOString();
1314+
}
1315+
if (object.expiresAt) {
1316+
object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() };
1317+
}
1318+
if (object._email_verify_token_expires_at) {
1319+
object._email_verify_token_expires_at = { __type: 'Date', iso: object._email_verify_token_expires_at.toISOString() };
1320+
}
1321+
if (object._account_lockout_expires_at) {
1322+
object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() };
1323+
}
1324+
if (object._perishable_token_expires_at) {
1325+
object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() };
1326+
}
1327+
if (object._password_changed_at) {
1328+
object._password_changed_at = { __type: 'Date', iso: object._password_changed_at.toISOString() };
1329+
}
13251330

1326-
for (const fieldName in object) {
1327-
if (object[fieldName] === null) {
1328-
delete object[fieldName];
1329-
}
1330-
if (object[fieldName] instanceof Date) {
1331-
object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() };
1332-
}
1333-
}
1331+
for (const fieldName in object) {
1332+
if (object[fieldName] === null) {
1333+
delete object[fieldName];
1334+
}
1335+
if (object[fieldName] instanceof Date) {
1336+
object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() };
1337+
}
1338+
}
13341339

1335-
return object;
1336-
}));
1340+
return object;
13371341
}
13381342

13391343
// Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
@@ -1406,10 +1410,10 @@ export class PostgresStorageAdapter {
14061410
}
14071411
const child = fieldName.split('.')[1];
14081412
return results.map(object => object[column][child]);
1409-
});
1413+
}).then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema)));
14101414
}
14111415

1412-
aggregate(className, pipeline) {
1416+
aggregate(className, schema, pipeline) {
14131417
debug('aggregate', className, pipeline);
14141418
const values = [className];
14151419
let columns = [];
@@ -1498,17 +1502,19 @@ export class PostgresStorageAdapter {
14981502

14991503
const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern} ${groupPattern}`;
15001504
debug(qs, values);
1501-
return this._client.any(qs, values).then(results => {
1502-
if (countField) {
1503-
results[0][countField] = parseInt(results[0][countField], 10);
1504-
}
1505-
results.forEach(result => {
1506-
if (!result.hasOwnProperty('objectId')) {
1507-
result.objectId = null;
1505+
return this._client.any(qs, values)
1506+
.then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema)))
1507+
.then(results => {
1508+
if (countField) {
1509+
results[0][countField] = parseInt(results[0][countField], 10);
15081510
}
1511+
results.forEach(result => {
1512+
if (!result.hasOwnProperty('objectId')) {
1513+
result.objectId = null;
1514+
}
1515+
});
1516+
return results;
15091517
});
1510-
return results;
1511-
});
15121518
}
15131519

15141520
performInitialization({ VolatileClassesSchemas }) {

src/Controllers/DatabaseController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ DatabaseController.prototype.find = function(className, query, {
875875
if (!classExists) {
876876
return [];
877877
} else {
878-
return this.adapter.aggregate(className, pipeline, readPreference);
878+
return this.adapter.aggregate(className, schema, pipeline, readPreference);
879879
}
880880
} else {
881881
if (!classExists) {

src/Routers/AggregateRouter.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import ClassesRouter from './ClassesRouter';
22
import rest from '../rest';
33
import * as middleware from '../middlewares';
44
import Parse from 'parse/node';
5+
import UsersRouter from './UsersRouter';
56

67
const ALLOWED_KEYS = [
78
'where',
@@ -65,8 +66,14 @@ export class AggregateRouter extends ClassesRouter {
6566
if (typeof body.where === 'string') {
6667
body.where = JSON.parse(body.where);
6768
}
68-
return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK)
69-
.then((response) => { return { response }; });
69+
return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK).then((response) => {
70+
for(const result of response.results) {
71+
if(typeof result === 'object') {
72+
UsersRouter.removeHiddenProperties(result);
73+
}
74+
}
75+
return { response };
76+
});
7077
}
7178

7279
mountRoutes() {

0 commit comments

Comments
 (0)