Skip to content

Commit 1bfbccf

Browse files
authored
fix: Custom object ID allows to acquire role privileges ([GHSA-8xq9-g7ch-35hg](GHSA-8xq9-g7ch-35hg)) (#9318)
1 parent 12ce46d commit 1bfbccf

File tree

5 files changed

+75
-92
lines changed

5 files changed

+75
-92
lines changed

Diff for: package-lock.json

+17-91
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"mongodb": "4.10.0",
4949
"mustache": "4.2.0",
5050
"otpauth": "9.2.2",
51-
"parse": "4.1.0",
51+
"parse": "4.2.0",
5252
"path-to-regexp": "6.2.1",
5353
"pg-monitor": "2.0.0",
5454
"pg-promise": "11.5.4",

Diff for: spec/vulnerabilities.spec.js

+45
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,51 @@
11
const request = require('../lib/request');
22

33
describe('Vulnerabilities', () => {
4+
describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => {
5+
beforeAll(async () => {
6+
await reconfigureServer({ allowCustomObjectId: true });
7+
Parse.allowCustomObjectId = true;
8+
});
9+
10+
afterAll(async () => {
11+
await reconfigureServer({ allowCustomObjectId: false });
12+
Parse.allowCustomObjectId = false;
13+
});
14+
15+
it('denies user creation with poisoned object ID', async () => {
16+
await expectAsync(
17+
new Parse.User({ id: 'role:a', username: 'a', password: '123' }).save()
18+
).toBeRejectedWith(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.'));
19+
});
20+
21+
describe('existing sessions for users with poisoned object ID', () => {
22+
/** @type {Parse.User} */
23+
let poisonedUser;
24+
/** @type {Parse.User} */
25+
let innocentUser;
26+
27+
beforeAll(async () => {
28+
const parseServer = await global.reconfigureServer();
29+
const databaseController = parseServer.config.databaseController;
30+
[poisonedUser, innocentUser] = await Promise.all(
31+
['role:abc', 'abc'].map(async id => {
32+
// Create the users directly on the db to bypass the user creation check
33+
await databaseController.create('_User', { objectId: id });
34+
// Use the master key to create a session for them to bypass the session check
35+
return Parse.User.loginAs(id);
36+
})
37+
);
38+
});
39+
40+
it('refuses session token of user with poisoned object ID', async () => {
41+
await expectAsync(
42+
new Parse.Query(Parse.User).find({ sessionToken: poisonedUser.getSessionToken() })
43+
).toBeRejectedWith(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.'));
44+
await new Parse.Query(Parse.User).find({ sessionToken: innocentUser.getSessionToken() });
45+
});
46+
});
47+
});
48+
449
describe('Object prototype pollution', () => {
550
it('denies object prototype to be polluted with keyword "constructor"', async () => {
651
const headers = {

Diff for: src/Auth.js

+5
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ const getAuthForSessionToken = async function ({
173173
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.');
174174
}
175175
const obj = session.user;
176+
177+
if (typeof obj['objectId'] === 'string' && obj['objectId'].startsWith('role:')) {
178+
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.');
179+
}
180+
176181
delete obj.password;
177182
obj['className'] = '_User';
178183
obj['sessionToken'] = sessionToken;

Diff for: src/Routers/ClassesRouter.js

+7
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ export class ClassesRouter extends PromiseRouter {
106106
}
107107

108108
handleCreate(req) {
109+
if (
110+
this.className(req) === '_User' &&
111+
typeof req.body?.objectId === 'string' &&
112+
req.body.objectId.startsWith('role:')
113+
) {
114+
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid object ID.');
115+
}
109116
return rest.create(
110117
req.config,
111118
req.auth,

0 commit comments

Comments
 (0)