Skip to content

Commit a0530c2

Browse files
authored
fix: Create CLP not enforced before user field validation on signup (#10268)
1 parent e8cc676 commit a0530c2

File tree

2 files changed

+122
-0
lines changed

2 files changed

+122
-0
lines changed

spec/vulnerabilities.spec.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2278,6 +2278,107 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint
22782278
});
22792279
});
22802280

2281+
describe('(GHSA-4m9m-p9j9-5hjw) User enumeration via signup endpoint', () => {
2282+
async function updateCLP(permissions) {
2283+
const response = await fetch(Parse.serverURL + '/schemas/_User', {
2284+
method: 'PUT',
2285+
headers: {
2286+
'X-Parse-Application-Id': Parse.applicationId,
2287+
'X-Parse-Master-Key': Parse.masterKey,
2288+
'Content-Type': 'application/json',
2289+
},
2290+
body: JSON.stringify({ classLevelPermissions: permissions }),
2291+
});
2292+
const body = await response.json();
2293+
if (body.error) {
2294+
throw body;
2295+
}
2296+
}
2297+
2298+
it('does not reveal existing username when public create CLP is disabled', async () => {
2299+
const user = new Parse.User();
2300+
user.setUsername('existingUser');
2301+
user.setPassword('password123');
2302+
await user.signUp();
2303+
await Parse.User.logOut();
2304+
2305+
await updateCLP({
2306+
get: { '*': true },
2307+
find: { '*': true },
2308+
create: {},
2309+
update: { '*': true },
2310+
delete: { '*': true },
2311+
addField: {},
2312+
});
2313+
2314+
const response = await request({
2315+
url: 'http://localhost:8378/1/classes/_User',
2316+
method: 'POST',
2317+
body: { username: 'existingUser', password: 'otherpassword' },
2318+
headers: {
2319+
'X-Parse-Application-Id': Parse.applicationId,
2320+
'X-Parse-REST-API-Key': 'rest',
2321+
'Content-Type': 'application/json',
2322+
},
2323+
}).catch(e => e);
2324+
expect(response.data.code).not.toBe(Parse.Error.USERNAME_TAKEN);
2325+
expect(response.data.error).not.toContain('Account already exists');
2326+
expect(response.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
2327+
});
2328+
2329+
it('does not reveal existing email when public create CLP is disabled', async () => {
2330+
const user = new Parse.User();
2331+
user.setUsername('emailUser');
2332+
user.setPassword('password123');
2333+
user.setEmail('existing@example.com');
2334+
await user.signUp();
2335+
await Parse.User.logOut();
2336+
2337+
await updateCLP({
2338+
get: { '*': true },
2339+
find: { '*': true },
2340+
create: {},
2341+
update: { '*': true },
2342+
delete: { '*': true },
2343+
addField: {},
2344+
});
2345+
2346+
const response = await request({
2347+
url: 'http://localhost:8378/1/classes/_User',
2348+
method: 'POST',
2349+
body: { username: 'newUser', password: 'otherpassword', email: 'existing@example.com' },
2350+
headers: {
2351+
'X-Parse-Application-Id': Parse.applicationId,
2352+
'X-Parse-REST-API-Key': 'rest',
2353+
'Content-Type': 'application/json',
2354+
},
2355+
}).catch(e => e);
2356+
expect(response.data.code).not.toBe(Parse.Error.EMAIL_TAKEN);
2357+
expect(response.data.error).not.toContain('Account already exists');
2358+
expect(response.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
2359+
});
2360+
2361+
it('still returns username taken error when public create CLP is enabled', async () => {
2362+
const user = new Parse.User();
2363+
user.setUsername('existingUser');
2364+
user.setPassword('password123');
2365+
await user.signUp();
2366+
await Parse.User.logOut();
2367+
2368+
const response = await request({
2369+
url: 'http://localhost:8378/1/classes/_User',
2370+
method: 'POST',
2371+
body: { username: 'existingUser', password: 'otherpassword' },
2372+
headers: {
2373+
'X-Parse-Application-Id': Parse.applicationId,
2374+
'X-Parse-REST-API-Key': 'rest',
2375+
'Content-Type': 'application/json',
2376+
},
2377+
}).catch(e => e);
2378+
expect(response.data.code).toBe(Parse.Error.USERNAME_TAKEN);
2379+
});
2380+
});
2381+
22812382
describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field name in PostgreSQL adapter', () => {
22822383
const headers = {
22832384
'Content-Type': 'application/json',

src/RestWrite.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ RestWrite.prototype.execute = function () {
131131
this.validSchemaController = schemaController;
132132
return this.setRequiredFieldsIfNeeded();
133133
})
134+
.then(() => {
135+
return this.validateCreatePermission();
136+
})
134137
.then(() => {
135138
return this.transformUser();
136139
})
@@ -698,6 +701,24 @@ RestWrite.prototype.checkRestrictedFields = async function () {
698701
}
699702
};
700703

704+
// Validates the create class-level permission before transformUser runs.
705+
// This prevents user enumeration (username/email existence) when public
706+
// create is disabled on _User, because transformUser checks uniqueness
707+
// before the CLP is enforced in runDatabaseOperation.
708+
RestWrite.prototype.validateCreatePermission = async function () {
709+
if (this.query || this.auth.isMaster || this.auth.isMaintenance) {
710+
return;
711+
}
712+
if (!this.validSchemaController) {
713+
return;
714+
}
715+
await this.validSchemaController.validatePermission(
716+
this.className,
717+
this.runOptions.acl || [],
718+
'create'
719+
);
720+
};
721+
701722
// The non-third-party parts of User transformation
702723
RestWrite.prototype.transformUser = async function () {
703724
var promise = Promise.resolve();

0 commit comments

Comments
 (0)