Skip to content

Commit 6ecc642

Browse files
authored
fix: Endpoint /sessions/me bypasses _Session protectedFields ([GHSA-g4v2-qx3q-4p64](GHSA-g4v2-qx3q-4p64)) (#10407)
1 parent 4b69661 commit 6ecc642

File tree

2 files changed

+105
-20
lines changed

2 files changed

+105
-20
lines changed

spec/vulnerabilities.spec.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5075,4 +5075,65 @@ describe('Vulnerabilities', () => {
50755075
expect(response.status).toBe(403);
50765076
});
50775077
});
5078+
5079+
describe('(GHSA-g4v2-qx3q-4p64) /sessions/me bypasses _Session protectedFields', () => {
5080+
const headers = {
5081+
'X-Parse-Application-Id': 'test',
5082+
'X-Parse-REST-API-Key': 'rest',
5083+
'Content-Type': 'application/json',
5084+
};
5085+
5086+
it('should not return protected fields on GET /sessions/me', async () => {
5087+
await reconfigureServer({
5088+
protectedFields: {
5089+
_Session: { '*': ['createdWith'] },
5090+
},
5091+
});
5092+
const user = new Parse.User();
5093+
user.setUsername('session-pf-user');
5094+
user.setPassword('password123');
5095+
user.set('email', 'session-pf@example.com');
5096+
await user.signUp();
5097+
const sessionToken = user.getSessionToken();
5098+
5099+
// Normal GET /sessions should strip createdWith
5100+
const sessionsResponse = await request({
5101+
method: 'GET',
5102+
url: 'http://localhost:8378/1/sessions',
5103+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken },
5104+
});
5105+
expect(sessionsResponse.data.results[0].createdWith).toBeUndefined();
5106+
5107+
// GET /sessions/me should also strip createdWith
5108+
const meResponse = await request({
5109+
method: 'GET',
5110+
url: 'http://localhost:8378/1/sessions/me',
5111+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken },
5112+
});
5113+
expect(meResponse.data.createdWith).toBeUndefined();
5114+
});
5115+
5116+
it('should return non-protected fields on GET /sessions/me', async () => {
5117+
await reconfigureServer({
5118+
protectedFields: {
5119+
_Session: { '*': ['createdWith'] },
5120+
},
5121+
});
5122+
const user = new Parse.User();
5123+
user.setUsername('session-pf-user2');
5124+
user.setPassword('password123');
5125+
user.set('email', 'session-pf2@example.com');
5126+
await user.signUp();
5127+
const sessionToken = user.getSessionToken();
5128+
5129+
const meResponse = await request({
5130+
method: 'GET',
5131+
url: 'http://localhost:8378/1/sessions/me',
5132+
headers: { ...headers, 'X-Parse-Session-Token': sessionToken },
5133+
});
5134+
expect(meResponse.data.sessionToken).toBe(sessionToken);
5135+
expect(meResponse.data.objectId).toBeDefined();
5136+
expect(meResponse.data.user).toBeDefined();
5137+
});
5138+
});
50785139
});

src/Routers/SessionsRouter.js

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,53 @@ export class SessionsRouter extends ClassesRouter {
99
return '_Session';
1010
}
1111

12-
handleMe(req) {
13-
// TODO: Verify correct behavior
12+
async handleMe(req) {
1413
if (!req.info || !req.info.sessionToken) {
1514
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token required.');
1615
}
17-
return rest
18-
.find(
19-
req.config,
20-
Auth.master(req.config),
21-
'_Session',
22-
{ sessionToken: req.info.sessionToken },
23-
undefined,
24-
req.info.clientSDK,
25-
req.info.context
26-
)
27-
.then(response => {
28-
if (!response.results || response.results.length == 0) {
29-
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token not found.');
30-
}
31-
return {
32-
response: response.results[0],
33-
};
34-
});
16+
const sessionToken = req.info.sessionToken;
17+
// Query with master key to validate the session token and get the session objectId
18+
const sessionResponse = await rest.find(
19+
req.config,
20+
Auth.master(req.config),
21+
'_Session',
22+
{ sessionToken },
23+
{},
24+
req.info.clientSDK,
25+
req.info.context
26+
);
27+
if (
28+
!sessionResponse.results ||
29+
sessionResponse.results.length == 0 ||
30+
!sessionResponse.results[0].user
31+
) {
32+
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token not found.');
33+
}
34+
const sessionObjectId = sessionResponse.results[0].objectId;
35+
const userId = sessionResponse.results[0].user.objectId;
36+
// Re-fetch the session with the caller's auth context so that
37+
// protectedFields and CLP apply correctly
38+
const userAuth = new Auth.Auth({
39+
config: req.config,
40+
isMaster: false,
41+
user: Parse.Object.fromJSON({ className: '_User', objectId: userId }),
42+
installationId: req.info.installationId,
43+
});
44+
const response = await rest.get(
45+
req.config,
46+
userAuth,
47+
'_Session',
48+
sessionObjectId,
49+
{},
50+
req.info.clientSDK,
51+
req.info.context
52+
);
53+
if (!response.results || response.results.length == 0) {
54+
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token not found.');
55+
}
56+
return {
57+
response: response.results[0],
58+
};
3559
}
3660

3761
handleUpdateToRevocableSession(req) {

0 commit comments

Comments
 (0)