Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions spec/vulnerabilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
21 changes: 21 additions & 0 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ RestWrite.prototype.execute = function () {
this.validSchemaController = schemaController;
return this.setRequiredFieldsIfNeeded();
})
.then(() => {
return this.validateCreatePermission();
})
.then(() => {
return this.transformUser();
})
Expand Down Expand Up @@ -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();
Expand Down
Loading