Parse Server vulnerable to remote code execution via MongoDB BSON parser through prototype pollution
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 5.5.2 and 6.2.1, an attacker can use a prototype pollution sink to trigger a remote code execution through the MongoDB BSON parser. A patch is available in versions 5.5.2 and 6.2.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | < 5.5.2 | 5.5.2 |
parse-servernpm | >= 6.0.0, < 6.2.1 | 6.2.1 |
Affected products
1- Range: < 5.5.2
Patches
25fad2928fb8efix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) (#8675)
6 files changed · +101 −33
.eslintrc.json+3 −0 modified@@ -24,5 +24,8 @@ "space-infix-ops": "error", "no-useless-escape": "off", "require-atomic-updates": "off" + }, + "globals": { + "Parse": true } }
spec/vulnerabilities.spec.js+65 −0 modified@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => { ); }); + it('denies creating global config with polluted data', async () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }; + const params = { + method: 'PUT', + url: 'http://localhost:8378/1/config', + json: true, + body: { + params: { + welcomeMesssage: 'Welcome to Parse', + foo: { _bsontype: 'Code', code: 'shell' }, + }, + }, + headers, + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe( + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ); + }); + + it('denies direct database write wih prohibited keys', async () => { + const Config = require('../lib/Config'); + const config = Config.get(Parse.applicationId); + const user = { + objectId: '1234567890', + username: 'hello', + password: 'pass', + _session_token: 'abc', + foo: { _bsontype: 'Code', code: 'shell' }, + }; + await expectAsync(config.database.create('_User', user)).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ) + ); + }); + + it('denies direct database update wih prohibited keys', async () => { + const Config = require('../lib/Config'); + const config = Config.get(Parse.applicationId); + const user = { + objectId: '1234567890', + username: 'hello', + password: 'pass', + _session_token: 'abc', + foo: { _bsontype: 'Code', code: 'shell' }, + }; + await expectAsync( + config.database.update('_User', { _id: user.objectId }, user) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ) + ); + }); + it('denies creating a hook with polluted data', async () => { const express = require('express'); const bodyParser = require('body-parser');
src/Controllers/DatabaseController.js+10 −0 modified@@ -467,6 +467,11 @@ class DatabaseController { validateOnly: boolean = false, validSchemaController: SchemaController.SchemaController ): Promise<any> { + try { + Utils.checkProhibitedKeywords(this.options, update); + } catch (error) { + return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + } const originalQuery = query; const originalUpdate = update; // Make a copy of the object, so we don't mutate the incoming data. @@ -797,6 +802,11 @@ class DatabaseController { validateOnly: boolean = false, validSchemaController: SchemaController.SchemaController ): Promise<any> { + try { + Utils.checkProhibitedKeywords(this.options, object); + } catch (error) { + return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + } // Make a copy of the object, so we don't mutate the incoming data. const originalObject = object; object = transformObjectACL(object);
src/RestWrite.js+5 −18 modified@@ -65,8 +65,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK } } - this.checkProhibitedKeywords(data); - // When the operation is complete, this.response may have several // fields. // response: the actual data to be returned @@ -288,7 +286,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { delete this.data.objectId; } } - this.checkProhibitedKeywords(this.data); + try { + Utils.checkProhibitedKeywords(this.config, this.data); + } catch (error) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error); + } }); }; @@ -1730,20 +1732,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { return response; }; -RestWrite.prototype.checkProhibitedKeywords = function (data) { - if (this.config.requestKeywordDenylist) { - // Scan request data for denied keywords - for (const keyword of this.config.requestKeywordDenylist) { - const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value); - if (match) { - throw new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Prohibited keyword in request data: ${JSON.stringify(keyword)}.` - ); - } - } - } -}; - export default RestWrite; module.exports = RestWrite;
src/Routers/FilesRouter.js+6 −15 modified@@ -173,22 +173,13 @@ export class FilesRouter { const base64 = req.body.toString('base64'); const file = new Parse.File(filename, { base64 }, contentType); const { metadata = {}, tags = {} } = req.fileData || {}; - if (req.config && req.config.requestKeywordDenylist) { + try { // Scan request data for denied keywords - for (const keyword of req.config.requestKeywordDenylist) { - const match = - Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) || - Utils.objectContainsKeyValue(tags, keyword.key, keyword.value); - if (match) { - next( - new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Prohibited keyword in request data: ${JSON.stringify(keyword)}.` - ) - ); - return; - } - } + Utils.checkProhibitedKeywords(config, metadata); + Utils.checkProhibitedKeywords(config, tags); + } catch (error) { + next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + return; } file.setTags(tags); file.setMetadata(metadata);
src/Utils.js+12 −0 modified@@ -358,6 +358,18 @@ class Utils { } return false; } + + static checkProhibitedKeywords(config, data) { + if (config?.requestKeywordDenylist) { + // Scan request data for denied keywords + for (const keyword of config.requestKeywordDenylist) { + const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value); + if (match) { + throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`; + } + } + } + } } module.exports = Utils;
3dd99dd80e27fix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) (#8674)
6 files changed · +101 −34
.eslintrc.json+3 −0 modified@@ -25,5 +25,8 @@ "space-infix-ops": "error", "no-useless-escape": "off", "require-atomic-updates": "off" + }, + "globals": { + "Parse": true } }
spec/vulnerabilities.spec.js+65 −0 modified@@ -138,6 +138,71 @@ describe('Vulnerabilities', () => { ); }); + it('denies creating global config with polluted data', async () => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }; + const params = { + method: 'PUT', + url: 'http://localhost:8378/1/config', + json: true, + body: { + params: { + welcomeMesssage: 'Welcome to Parse', + foo: { _bsontype: 'Code', code: 'shell' }, + }, + }, + headers, + }; + const response = await request(params).catch(e => e); + expect(response.status).toBe(400); + const text = JSON.parse(response.text); + expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME); + expect(text.error).toBe( + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ); + }); + + it('denies direct database write wih prohibited keys', async () => { + const Config = require('../lib/Config'); + const config = Config.get(Parse.applicationId); + const user = { + objectId: '1234567890', + username: 'hello', + password: 'pass', + _session_token: 'abc', + foo: { _bsontype: 'Code', code: 'shell' }, + }; + await expectAsync(config.database.create('_User', user)).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ) + ); + }); + + it('denies direct database update wih prohibited keys', async () => { + const Config = require('../lib/Config'); + const config = Config.get(Parse.applicationId); + const user = { + objectId: '1234567890', + username: 'hello', + password: 'pass', + _session_token: 'abc', + foo: { _bsontype: 'Code', code: 'shell' }, + }; + await expectAsync( + config.database.update('_User', { _id: user.objectId }, user) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + 'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.' + ) + ); + }); + it('denies creating a hook with polluted data', async () => { const express = require('express'); const bodyParser = require('body-parser');
src/Controllers/DatabaseController.js+10 −0 modified@@ -475,6 +475,11 @@ class DatabaseController { validateOnly: boolean = false, validSchemaController: SchemaController.SchemaController ): Promise<any> { + try { + Utils.checkProhibitedKeywords(this.options, update); + } catch (error) { + return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + } const originalQuery = query; const originalUpdate = update; // Make a copy of the object, so we don't mutate the incoming data. @@ -805,6 +810,11 @@ class DatabaseController { validateOnly: boolean = false, validSchemaController: SchemaController.SchemaController ): Promise<any> { + try { + Utils.checkProhibitedKeywords(this.options, object); + } catch (error) { + return Promise.reject(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + } // Make a copy of the object, so we don't mutate the incoming data. const originalObject = object; object = transformObjectACL(object);
src/RestWrite.js+5 −18 modified@@ -64,8 +64,6 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK } } - this.checkProhibitedKeywords(data); - // When the operation is complete, this.response may have several // fields. // response: the actual data to be returned @@ -298,7 +296,11 @@ RestWrite.prototype.runBeforeSaveTrigger = function () { delete this.data.objectId; } } - this.checkProhibitedKeywords(this.data); + try { + Utils.checkProhibitedKeywords(this.config, this.data); + } catch (error) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, error); + } }); }; @@ -1756,20 +1758,5 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { return response; }; -RestWrite.prototype.checkProhibitedKeywords = function (data) { - if (this.config.requestKeywordDenylist) { - // Scan request data for denied keywords - for (const keyword of this.config.requestKeywordDenylist) { - const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value); - if (match) { - throw new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Prohibited keyword in request data: ${JSON.stringify(keyword)}.` - ); - } - } - } -}; - export default RestWrite; module.exports = RestWrite;
src/Routers/FilesRouter.js+6 −16 modified@@ -175,22 +175,12 @@ export class FilesRouter { const base64 = req.body.toString('base64'); const file = new Parse.File(filename, { base64 }, contentType); const { metadata = {}, tags = {} } = req.fileData || {}; - if (req.config && req.config.requestKeywordDenylist) { - // Scan request data for denied keywords - for (const keyword of req.config.requestKeywordDenylist) { - const match = - Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) || - Utils.objectContainsKeyValue(tags, keyword.key, keyword.value); - if (match) { - next( - new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - `Prohibited keyword in request data: ${JSON.stringify(keyword)}.` - ) - ); - return; - } - } + try { + Utils.checkProhibitedKeywords(config, metadata); + Utils.checkProhibitedKeywords(config, tags); + } catch (error) { + next(new Parse.Error(Parse.Error.INVALID_KEY_NAME, error)); + return; } file.setTags(tags); file.setMetadata(metadata);
src/Utils.js+12 −0 modified@@ -358,6 +358,18 @@ class Utils { } return false; } + + static checkProhibitedKeywords(config, data) { + if (config?.requestKeywordDenylist) { + // Scan request data for denied keywords + for (const keyword of config.requestKeywordDenylist) { + const match = Utils.objectContainsKeyValue(data, keyword.key, keyword.value); + if (match) { + throw `Prohibited keyword in request data: ${JSON.stringify(keyword)}.`; + } + } + } + } } module.exports = Utils;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-462x-c3jw-7vr6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-36475ghsaADVISORY
- github.com/parse-community/parse-server/commit/3dd99dd80e27e5e1d99b42844180546d90c7aa90ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/5fad2928fb8ee17304abcdcf259932f827d8c81fghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/issues/8674ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/issues/8675ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/releases/tag/5.5.2ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/releases/tag/6.2.1ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.