Skip to content

Commit 6ae5835

Browse files
authored
Merge pull request from GHSA-xqp8-w826-hh6x
* Backport the advisory fix * Added a 4.10.3 section to CHANGELOG
1 parent 0bfa6b7 commit 6ae5835

File tree

4 files changed

+73
-1
lines changed

4 files changed

+73
-1
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Parse Server Changelog
22

3+
# 4.10.3
4+
5+
## Security Fixes
6+
- Validate `explain` query parameter to avoid a server crash due to MongoDB bug [NODE-3463](https://jira.mongodb.org/browse/NODE-3463) (Kartal Kaan Bozdogan) [GHSA-xqp8-w826-hh6x](https://github.com/parse-community/parse-server/security/advisories/GHSA-xqp8-w826-hh6x)
7+
38
# 4.10.2
49
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.1...4.10.2)
510

spec/ParseQuery.spec.js

+48
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,54 @@ describe('Parse.Query testing', () => {
3333
});
3434
});
3535

36+
it_only_db('mongo')('gracefully handles invalid explain values', async () => {
37+
// Note that anything that is not truthy (like 0) does not cause an exception, as they get swallowed up by ClassesRouter::optionsFromBody
38+
const values = [1, 'yolo', { a: 1 }, [1, 2, 3]];
39+
for (const value of values) {
40+
try {
41+
await request({
42+
method: 'GET',
43+
url: `http://localhost:8378/1/classes/_User?explain=${value}`,
44+
json: true,
45+
headers: masterKeyHeaders,
46+
});
47+
fail('request did not throw');
48+
} catch (e) {
49+
// Expect that Parse Server did not crash
50+
expect(e.code).not.toEqual('ECONNRESET');
51+
// Expect that Parse Server validates the explain value and does not crash;
52+
// see https://jira.mongodb.org/browse/NODE-3463
53+
equal(e.data.code, Parse.Error.INVALID_QUERY);
54+
equal(e.data.error, 'Invalid value for explain');
55+
}
56+
// get queries (of the form '/classes/:className/:objectId' cannot have the explain key, see ClassesRouter.js)
57+
// so it is enough that we test find queries
58+
}
59+
});
60+
61+
it_only_db('mongo')('supports valid explain values', async () => {
62+
const values = [
63+
false,
64+
true,
65+
'queryPlanner',
66+
'executionStats',
67+
'allPlansExecution',
68+
// 'queryPlannerExtended' is excluded as it only applies to MongoDB Data Lake which is currently not available in our CI environment
69+
];
70+
for (const value of values) {
71+
const response = await request({
72+
method: 'GET',
73+
url: `http://localhost:8378/1/classes/_User?explain=${value}`,
74+
json: true,
75+
headers: masterKeyHeaders,
76+
});
77+
expect(response.status).toBe(200);
78+
if (value) {
79+
expect(response.data.results.ok).toBe(1);
80+
}
81+
}
82+
});
83+
3684
it('searching for null', function (done) {
3785
const baz = new TestObject({ foo: null });
3886
const qux = new TestObject({ foo: 'qux' });

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

+19
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (
108108
return mongoObject;
109109
};
110110

111+
function validateExplainValue(explain) {
112+
if (explain) {
113+
// The list of allowed explain values is from node-mongodb-native/lib/explain.js
114+
const explainAllowedValues = [
115+
'queryPlanner',
116+
'queryPlannerExtended',
117+
'executionStats',
118+
'allPlansExecution',
119+
false,
120+
true,
121+
];
122+
if (!explainAllowedValues.includes(explain)) {
123+
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Invalid value for explain');
124+
}
125+
}
126+
}
127+
111128
export class MongoStorageAdapter implements StorageAdapter {
112129
// Private
113130
_uri: string;
@@ -562,6 +579,7 @@ export class MongoStorageAdapter implements StorageAdapter {
562579
query: QueryType,
563580
{ skip, limit, sort, keys, readPreference, hint, caseInsensitive, explain }: QueryOptions
564581
): Promise<any> {
582+
validateExplainValue(explain);
565583
schema = convertParseSchemaToMongoSchema(schema);
566584
const mongoWhere = transformWhere(className, query, schema);
567585
const mongoSort = _.mapKeys(sort, (value, fieldName) =>
@@ -740,6 +758,7 @@ export class MongoStorageAdapter implements StorageAdapter {
740758
hint: ?mixed,
741759
explain?: boolean
742760
) {
761+
validateExplainValue(explain);
743762
let isPointerField = false;
744763
pipeline = pipeline.map(stage => {
745764
if (stage.$group) {

src/RestQuery.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ RestQuery.prototype.runFind = function (options = {}) {
633633
return this.config.database
634634
.find(this.className, this.restWhere, findOptions, this.auth)
635635
.then(results => {
636-
if (this.className === '_User' && findOptions.explain !== true) {
636+
if (this.className === '_User' && !findOptions.explain) {
637637
for (var result of results) {
638638
cleanResultAuthData(result);
639639
}

0 commit comments

Comments
 (0)