From 60afe9b52f559d664dccbd18964eb2eea49a0b8b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 21 Mar 2026 04:34:14 +0000 Subject: [PATCH] fix: User enumeration via signup when create CLP is disabled --- spec/vulnerabilities.spec.js | 101 +++++++++++++++++++++++++++++++++++ src/RestWrite.js | 21 ++++++++ 2 files changed, 122 insertions(+) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 6c734f308b..0f96ea9ee3 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2278,6 +2278,107 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint }); }); +describe('(GHSA-4m9m-p9j9-5hjw) User enumeration via signup endpoint', () => { + async function updateCLP(permissions) { + const response = await fetch(Parse.serverURL + '/schemas/_User', { + method: 'PUT', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ classLevelPermissions: permissions }), + }); + const body = await response.json(); + if (body.error) { + throw body; + } + } + + it('does not reveal existing username when public create CLP is disabled', async () => { + const user = new Parse.User(); + user.setUsername('existingUser'); + user.setPassword('password123'); + await user.signUp(); + await Parse.User.logOut(); + + await updateCLP({ + get: { '*': true }, + find: { '*': true }, + create: {}, + update: { '*': true }, + delete: { '*': true }, + addField: {}, + }); + + const response = await request({ + url: 'http://localhost:8378/1/classes/_User', + method: 'POST', + body: { username: 'existingUser', password: 'otherpassword' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }).catch(e => e); + expect(response.data.code).not.toBe(Parse.Error.USERNAME_TAKEN); + expect(response.data.error).not.toContain('Account already exists'); + expect(response.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + }); + + it('does not reveal existing email when public create CLP is disabled', async () => { + const user = new Parse.User(); + user.setUsername('emailUser'); + user.setPassword('password123'); + user.setEmail('existing@example.com'); + await user.signUp(); + await Parse.User.logOut(); + + await updateCLP({ + get: { '*': true }, + find: { '*': true }, + create: {}, + update: { '*': true }, + delete: { '*': true }, + addField: {}, + }); + + const response = await request({ + url: 'http://localhost:8378/1/classes/_User', + method: 'POST', + body: { username: 'newUser', password: 'otherpassword', email: 'existing@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }).catch(e => e); + expect(response.data.code).not.toBe(Parse.Error.EMAIL_TAKEN); + expect(response.data.error).not.toContain('Account already exists'); + expect(response.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + }); + + it('still returns username taken error when public create CLP is enabled', async () => { + const user = new Parse.User(); + user.setUsername('existingUser'); + user.setPassword('password123'); + await user.signUp(); + await Parse.User.logOut(); + + const response = await request({ + url: 'http://localhost:8378/1/classes/_User', + method: 'POST', + body: { username: 'existingUser', password: 'otherpassword' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }).catch(e => e); + expect(response.data.code).toBe(Parse.Error.USERNAME_TAKEN); + }); +}); + describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field name in PostgreSQL adapter', () => { const headers = { 'Content-Type': 'application/json', diff --git a/src/RestWrite.js b/src/RestWrite.js index 11794000b0..bdbac02148 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -131,6 +131,9 @@ RestWrite.prototype.execute = function () { this.validSchemaController = schemaController; return this.setRequiredFieldsIfNeeded(); }) + .then(() => { + return this.validateCreatePermission(); + }) .then(() => { return this.transformUser(); }) @@ -698,6 +701,24 @@ RestWrite.prototype.checkRestrictedFields = async function () { } }; +// Validates the create class-level permission before transformUser runs. +// This prevents user enumeration (username/email existence) when public +// create is disabled on _User, because transformUser checks uniqueness +// before the CLP is enforced in runDatabaseOperation. +RestWrite.prototype.validateCreatePermission = async function () { + if (this.query || this.auth.isMaster || this.auth.isMaintenance) { + return; + } + if (!this.validSchemaController) { + return; + } + await this.validSchemaController.validatePermission( + this.className, + this.runOptions.acl || [], + 'create' + ); +}; + // The non-third-party parts of User transformation RestWrite.prototype.transformUser = async function () { var promise = Promise.resolve();