From 1bfbccf9ee7ea77533b2b2aa7c4c69f3bd35e66f Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 3 Oct 2024 21:28:41 +0200 Subject: [PATCH] fix: 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) --- package-lock.json | 108 ++++++----------------------------- package.json | 2 +- spec/vulnerabilities.spec.js | 45 +++++++++++++++ src/Auth.js | 5 ++ src/Routers/ClassesRouter.js | 7 +++ 5 files changed, 75 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index f887b4dd27..74cf825f88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": {} } } diff --git a/package.json b/package.json index 60accfe810..cf593d8fc9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index d6c7971cad..700b83776c 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -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 = { diff --git a/src/Auth.js b/src/Auth.js index 6488b8427e..43fe6b7c94 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -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; diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 5dfba97ee0..53de503234 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -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,