diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 5017fe7230..5eb3c6cb93 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 c5351fe9fc..3177597f70 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -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; diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 0a68e25254..1f9f93e329 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,