Parse Server's custom object ID allows to acquire role privileges
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. If the Parse Server option allowCustomObjectId: true is set, an attacker that is allowed to create a new user can set a custom object ID for that new user that exploits the vulnerability and acquires privileges of a specific role. This vulnerability is fixed in 6.5.9 and 7.3.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | < 6.5.9 | 6.5.9 |
parse-servernpm | >= 7.0.0, < 7.3.0 | 7.3.0 |
Affected products
1- Range: < 6.5.9
Patches
21bfbccf9ee7efix: Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) (#9318)
5 files changed · +75 −92
package.json+1 −1 modified@@ -48,7 +48,7 @@ "mongodb": "4.10.0", "mustache": "4.2.0", "otpauth": "9.2.2", - "parse": "4.1.0", + "parse": "4.2.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", "pg-promise": "11.5.4",
package-lock.json+17 −91 modified@@ -18,11 +18,11 @@ "@parse/fs-files-adapter": "2.0.1", "@parse/push-adapter": "5.1.1", "bcryptjs": "2.4.3", - "body-parser": "^1.20.3", + "body-parser": "1.20.3", "commander": "10.0.1", "cors": "2.8.5", "deepcopy": "2.1.0", - "express": "^4.21.0", + "express": "4.21.0", "express-rate-limit": "6.7.0", "follow-redirects": "1.15.6", "graphql": "16.8.1", @@ -39,7 +39,7 @@ "mongodb": "4.10.0", "mustache": "4.2.0", "otpauth": "9.2.2", - "parse": "4.1.0", + "parse": "4.2.0", "path-to-regexp": "6.2.1", "pg-monitor": "2.0.0", "pg-promise": "11.5.4", @@ -3099,53 +3099,6 @@ "node": ">= 14" } }, - "node_modules/@parse/push-adapter/node_modules/parse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-4.2.0.tgz", - "integrity": "sha512-K8bWs0wM2qRhkSr6N16j8OvsF6Uallrynqng9e+tzR3RdKuB09vaJh48qrf9MbiJ1Ya4JZI7AfEHYF+ywEKs7Q==", - "dependencies": { - "@babel/runtime-corejs3": "7.21.0", - "idb-keyval": "6.2.0", - "react-native-crypto-js": "1.0.0", - "uuid": "9.0.0", - "ws": "8.13.0", - "xmlhttprequest": "1.8.0" - }, - "engines": { - "node": ">=14.21.0 <17 || >=18 <20" - }, - "optionalDependencies": { - "crypto-js": "4.1.1" - } - }, - "node_modules/@parse/push-adapter/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@parse/push-adapter/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -16946,15 +16899,15 @@ } }, "node_modules/parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-4.1.0.tgz", - "integrity": "sha512-s0Ti+nWrKWj9DlFcmkEE05fGwa/K5ycZSdqCz01F8YL7Hevqv4WLXAmYGOwzq5UJSZ005seKgb20KwVwLdy/Zg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-4.2.0.tgz", + "integrity": "sha512-K8bWs0wM2qRhkSr6N16j8OvsF6Uallrynqng9e+tzR3RdKuB09vaJh48qrf9MbiJ1Ya4JZI7AfEHYF+ywEKs7Q==", "dependencies": { "@babel/runtime-corejs3": "7.21.0", "idb-keyval": "6.2.0", "react-native-crypto-js": "1.0.0", "uuid": "9.0.0", - "ws": "8.12.0", + "ws": "8.13.0", "xmlhttprequest": "1.8.0" }, "engines": { @@ -17000,9 +16953,9 @@ } }, "node_modules/parse/node_modules/ws": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", - "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "engines": { "node": ">=10.0.0" }, @@ -23970,33 +23923,6 @@ "firebase-admin": "12.0.0", "npmlog": "7.0.1", "parse": "4.2.0" - }, - "dependencies": { - "parse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-4.2.0.tgz", - "integrity": "sha512-K8bWs0wM2qRhkSr6N16j8OvsF6Uallrynqng9e+tzR3RdKuB09vaJh48qrf9MbiJ1Ya4JZI7AfEHYF+ywEKs7Q==", - "requires": { - "@babel/runtime-corejs3": "7.21.0", - "crypto-js": "4.1.1", - "idb-keyval": "6.2.0", - "react-native-crypto-js": "1.0.0", - "uuid": "9.0.0", - "ws": "8.13.0", - "xmlhttprequest": "1.8.0" - } - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - }, - "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "requires": {} - } } }, "@protobufjs/aspromise": { @@ -34573,16 +34499,16 @@ } }, "parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-4.1.0.tgz", - "integrity": "sha512-s0Ti+nWrKWj9DlFcmkEE05fGwa/K5ycZSdqCz01F8YL7Hevqv4WLXAmYGOwzq5UJSZ005seKgb20KwVwLdy/Zg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-4.2.0.tgz", + "integrity": "sha512-K8bWs0wM2qRhkSr6N16j8OvsF6Uallrynqng9e+tzR3RdKuB09vaJh48qrf9MbiJ1Ya4JZI7AfEHYF+ywEKs7Q==", "requires": { "@babel/runtime-corejs3": "7.21.0", "crypto-js": "4.1.1", "idb-keyval": "6.2.0", "react-native-crypto-js": "1.0.0", "uuid": "9.0.0", - "ws": "8.12.0", + "ws": "8.13.0", "xmlhttprequest": "1.8.0" }, "dependencies": { @@ -34592,9 +34518,9 @@ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" }, "ws": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", - "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "requires": {} } }
spec/vulnerabilities.spec.js+45 −0 modified@@ -1,6 +1,51 @@ const request = require('../lib/request'); describe('Vulnerabilities', () => { + describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => { + beforeAll(async () => { + await reconfigureServer({ allowCustomObjectId: true }); + Parse.allowCustomObjectId = true; + }); + + afterAll(async () => { + await reconfigureServer({ allowCustomObjectId: false }); + Parse.allowCustomObjectId = false; + }); + + it('denies user creation with poisoned object ID', async () => { + await expectAsync( + new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save() + ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.')); + }); + + describe('existing sessions for users with poisoned object ID', () => { + /** @type {Parse.User} */ + let poisonedUser; + /** @type {Parse.User} */ + let innocentUser; + + beforeAll(async () => { + const parseServer = await global.reconfigureServer(); + const databaseController = parseServer.config.databaseController; + [poisonedUser, innocentUser] = await Promise.all( + ['role:abc', 'abc'].map(async id => { + // Create the users directly on the db to bypass the user creation check + await databaseController.create('_User', { objectId: id }); + // Use the master key to create a session for them to bypass the session check + return Parse.User.loginAs(id); + }) + ); + }); + + it('refuses session token of user with poisoned object ID', async () => { + await expectAsync( + new Parse.Query(Parse.User).find({ sessionToken: poisonedUser.getSessionToken() }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.')); + await new Parse.Query(Parse.User).find({ sessionToken: innocentUser.getSessionToken() }); + }); + }); + }); + describe('Object prototype pollution', () => { it('denies object prototype to be polluted with keyword "constructor"', async () => { const headers = {
src/Auth.js+5 −0 modified@@ -173,6 +173,11 @@ const getAuthForSessionToken = async function ({ throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.'); } const obj = session.user; + + if (typeof obj['objectId'] === 'string' && obj['objectId'].startsWith('role:')) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.'); + } + delete obj.password; obj['className'] = '_User'; obj['sessionToken'] = sessionToken;
src/Routers/ClassesRouter.js+7 −0 modified@@ -106,6 +106,13 @@ export class ClassesRouter extends PromiseRouter { } handleCreate(req) { + if ( + this.className(req) === '_User' && + typeof req.body?.objectId === 'string' && + req.body.objectId.startsWith('role:') + ) { + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); + } return rest.create( req.config, req.auth,
13ee52f0d19efix: Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](https://github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hg)) (#9317)
3 files changed · +57 −0
spec/vulnerabilities.spec.js+45 −0 modified@@ -1,6 +1,51 @@ const request = require('../lib/request'); describe('Vulnerabilities', () => { + describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => { + beforeAll(async () => { + await reconfigureServer({ allowCustomObjectId: true }); + Parse.allowCustomObjectId = true; + }); + + afterAll(async () => { + await reconfigureServer({ allowCustomObjectId: false }); + Parse.allowCustomObjectId = false; + }); + + it('denies user creation with poisoned object ID', async () => { + await expectAsync( + new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save() + ).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.')); + }); + + describe('existing sessions for users with poisoned object ID', () => { + /** @type {Parse.User} */ + let poisonedUser; + /** @type {Parse.User} */ + let innocentUser; + + beforeAll(async () => { + const parseServer = await global.reconfigureServer(); + const databaseController = parseServer.config.databaseController; + [poisonedUser, innocentUser] = await Promise.all( + ['role:abc', 'abc'].map(async id => { + // Create the users directly on the db to bypass the user creation check + await databaseController.create('_User', { objectId: id }); + // Use the master key to create a session for them to bypass the session check + return Parse.User.loginAs(id); + }) + ); + }); + + it('refuses session token of user with poisoned object ID', async () => { + await expectAsync( + new Parse.Query(Parse.User).find({ sessionToken: poisonedUser.getSessionToken() }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.')); + await new Parse.Query(Parse.User).find({ sessionToken: innocentUser.getSessionToken() }); + }); + }); + }); + describe('Object prototype pollution', () => { it('denies object prototype to be polluted with keyword "constructor"', async () => { const headers = {
src/Auth.js+5 −0 modified@@ -180,6 +180,11 @@ const getAuthForSessionToken = async function ({ throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.'); } const obj = session.user; + + if (typeof obj['objectId'] === 'string' && obj['objectId'].startsWith('role:')) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.'); + } + delete obj.password; obj['className'] = '_User'; obj['sessionToken'] = sessionToken;
src/Routers/ClassesRouter.js+7 −0 modified@@ -106,6 +106,13 @@ export class ClassesRouter extends PromiseRouter { } handleCreate(req) { + if ( + this.className(req) === '_User' && + typeof req.body?.objectId === 'string' && + req.body.objectId.startsWith('role:') + ) { + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'); + } return rest.create( req.config, req.auth,
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
7- github.com/advisories/GHSA-8xq9-g7ch-35hgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-47183ghsaADVISORY
- github.com/parse-community/parse-server/commit/13ee52f0d19ef3a3524b3d79aea100e587eb3cfcghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/1bfbccf9ee7ea77533b2b2aa7c4c69f3bd35e66fghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/9317ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/9318ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-8xq9-g7ch-35hgghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.