Skip to content

Commit 5fad292

Browse files
authored
fix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](GHSA-462x-c3jw-7vr6) (#8675)
1 parent a036071 commit 5fad292

File tree

6 files changed

+101
-33
lines changed

6 files changed

+101
-33
lines changed

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@
2424
"space-infix-ops": "error",
2525
"no-useless-escape": "off",
2626
"require-atomic-updates": "off"
27+
},
28+
"globals": {
29+
"Parse": true
2730
}
2831
}

spec/vulnerabilities.spec.js

+65
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
138138
);
139139
});
140140

141+
it('denies creating global config with polluted data', async () => {
142+
const headers = {
143+
'Content-Type': 'application/json',
144+
'X-Parse-Application-Id': 'test',
145+
'X-Parse-Master-Key': 'test',
146+
};
147+
const params = {
148+
method: 'PUT',
149+
url: 'http://localhost:8378/1/config',
150+
json: true,
151+
body: {
152+
params: {
153+
welcomeMesssage: 'Welcome to Parse',
154+
foo: { _bsontype: 'Code', code: 'shell' },
155+
},
156+
},
157+
headers,
158+
};
159+
const response = await request(params).catch(e => e);
160+
expect(response.status).toBe(400);
161+
const text = JSON.parse(response.text);
162+
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
163+
expect(text.error).toBe(
164+
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
165+
);
166+
});
167+
168+
it('denies direct database write wih prohibited keys', async () => {
169+
const Config = require('../lib/Config');
170+
const config = Config.get(Parse.applicationId);
171+
const user = {
172+
objectId: '1234567890',
173+
username: 'hello',
174+
password: 'pass',
175+
_session_token: 'abc',
176+
foo: { _bsontype: 'Code', code: 'shell' },
177+
};
178+
await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
179+
new Parse.Error(
180+
Parse.Error.INVALID_KEY_NAME,
181+
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
182+
)
183+
);
184+
});
185+
186+
it('denies direct database update wih prohibited keys', async () => {
187+
const Config = require('../lib/Config');
188+
const config = Config.get(Parse.applicationId);
189+
const user = {
190+
objectId: '1234567890',
191+
username: 'hello',
192+
password: 'pass',
193+
_session_token: 'abc',
194+
foo: { _bsontype: 'Code', code: 'shell' },
195+
};
196+
await expectAsync(
197+
config.database.update('_User', { _id: user.objectId }, user)
198+
).toBeRejectedWith(
199+
new Parse.Error(
200+
Parse.Error.INVALID_KEY_NAME,
201+
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
202+
)
203+
);
204+
});
205+
141206
it('denies creating a hook with polluted data', async () => {
142207
const express = require('express');
143208
const bodyParser = require('body-parser');

src/Controllers/DatabaseController.js

+10
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,11 @@ class DatabaseController {
467467
validateOnly: boolean = false,
468468
validSchemaController: SchemaController.SchemaController
469469
): Promise<any> {
470+
try {
471+
Utils.checkProhibitedKeywords(this.options, update);
472+
} catch (error) {
473+
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
474+
}
470475
const originalQuery = query;
471476
const originalUpdate = update;
472477
// Make a copy of the object, so we don't mutate the incoming data.
@@ -797,6 +802,11 @@ class DatabaseController {
797802
validateOnly: boolean = false,
798803
validSchemaController: SchemaController.SchemaController
799804
): Promise<any> {
805+
try {
806+
Utils.checkProhibitedKeywords(this.options, object);
807+
} catch (error) {
808+
return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
809+
}
800810
// Make a copy of the object, so we don't mutate the incoming data.
801811
const originalObject = object;
802812
object = transformObjectACL(object);

src/RestWrite.js

+5-18
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
6565
}
6666
}
6767

68-
this.checkProhibitedKeywords(data);
69-
7068
// When the operation is complete, this.response may have several
7169
// fields.
7270
// response: the actual data to be returned
@@ -288,7 +286,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () {
288286
delete this.data.objectId;
289287
}
290288
}
291-
this.checkProhibitedKeywords(this.data);
289+
try {
290+
Utils.checkProhibitedKeywords(this.config, this.data);
291+
} catch (error) {
292+
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error);
293+
}
292294
});
293295
};
294296

@@ -1730,20 +1732,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) {
17301732
return response;
17311733
};
17321734

1733-
RestWrite.prototype.checkProhibitedKeywords = function (data) {
1734-
if (this.config.requestKeywordDenylist) {
1735-
// Scan request data for denied keywords
1736-
for (const keyword of this.config.requestKeywordDenylist) {
1737-
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
1738-
if (match) {
1739-
throw new Parse.Error(
1740-
Parse.Error.INVALID_KEY_NAME,
1741-
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
1742-
);
1743-
}
1744-
}
1745-
}
1746-
};
1747-
17481735
export default RestWrite;
17491736
module.exports = RestWrite;

src/Routers/FilesRouter.js

+6-15
Original file line numberDiff line numberDiff line change
@@ -173,22 +173,13 @@ export class FilesRouter {
173173
const base64 = req.body.toString('base64');
174174
const file = new Parse.File(filename, { base64 }, contentType);
175175
const { metadata = {}, tags = {} } = req.fileData || {};
176-
if (req.config && req.config.requestKeywordDenylist) {
176+
try {
177177
// Scan request data for denied keywords
178-
for (const keyword of req.config.requestKeywordDenylist) {
179-
const match =
180-
Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
181-
Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
182-
if (match) {
183-
next(
184-
new Parse.Error(
185-
Parse.Error.INVALID_KEY_NAME,
186-
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
187-
)
188-
);
189-
return;
190-
}
191-
}
178+
Utils.checkProhibitedKeywords(config, metadata);
179+
Utils.checkProhibitedKeywords(config, tags);
180+
} catch (error) {
181+
next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error));
182+
return;
192183
}
193184
file.setTags(tags);
194185
file.setMetadata(metadata);

src/Utils.js

+12
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,18 @@ class Utils {
358358
}
359359
return false;
360360
}
361+
362+
static checkProhibitedKeywords(config, data) {
363+
if (config?.requestKeywordDenylist) {
364+
// Scan request data for denied keywords
365+
for (const keyword of config.requestKeywordDenylist) {
366+
const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value);
367+
if (match) {
368+
throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`;
369+
}
370+
}
371+
}
372+
}
361373
}
362374

363375
module.exports = Utils;

0 commit comments

Comments
 (0)