diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 97c2567fee..30a28fee1a 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -5075,4 +5075,65 @@ describe('Vulnerabilities', () => { expect(response.status).toBe(403); }); }); + + describe('(GHSA-g4v2-qx3q-4p64) /sessions/me bypasses _Session protectedFields', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }; + + it('should not return protected fields on GET /sessions/me', async () => { + await reconfigureServer({ + protectedFields: { + _Session: { '*': ['createdWith'] }, + }, + }); + const user = new Parse.User(); + user.setUsername('session-pf-user'); + user.setPassword('password123'); + user.set('email', 'session-pf@example.com'); + await user.signUp(); + const sessionToken = user.getSessionToken(); + + // Normal GET /sessions should strip createdWith + const sessionsResponse = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions', + headers: { ...headers, 'X-Parse-Session-Token': sessionToken }, + }); + expect(sessionsResponse.data.results[0].createdWith).toBeUndefined(); + + // GET /sessions/me should also strip createdWith + const meResponse = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { ...headers, 'X-Parse-Session-Token': sessionToken }, + }); + expect(meResponse.data.createdWith).toBeUndefined(); + }); + + it('should return non-protected fields on GET /sessions/me', async () => { + await reconfigureServer({ + protectedFields: { + _Session: { '*': ['createdWith'] }, + }, + }); + const user = new Parse.User(); + user.setUsername('session-pf-user2'); + user.setPassword('password123'); + user.set('email', 'session-pf2@example.com'); + await user.signUp(); + const sessionToken = user.getSessionToken(); + + const meResponse = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { ...headers, 'X-Parse-Session-Token': sessionToken }, + }); + expect(meResponse.data.sessionToken).toBe(sessionToken); + expect(meResponse.data.objectId).toBeDefined(); + expect(meResponse.data.user).toBeDefined(); + }); + }); }); diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js index e5275fae06..a0b1eb2af3 100644 --- a/src/Routers/SessionsRouter.js +++ b/src/Routers/SessionsRouter.js @@ -9,29 +9,53 @@ export class SessionsRouter extends ClassesRouter { return '_Session'; } - handleMe(req) { - // TODO: Verify correct behavior + async handleMe(req) { if (!req.info || !req.info.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token required.'); } - return rest - .find( - req.config, - Auth.master(req.config), - '_Session', - { sessionToken: req.info.sessionToken }, - undefined, - req.info.clientSDK, - req.info.context - ) - .then(response => { - if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token not found.'); - } - return { - response: response.results[0], - }; - }); + const sessionToken = req.info.sessionToken; + // Query with master key to validate the session token and get the session objectId + const sessionResponse = await rest.find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken }, + {}, + req.info.clientSDK, + req.info.context + ); + if ( + !sessionResponse.results || + sessionResponse.results.length == 0 || + !sessionResponse.results[0].user + ) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token not found.'); + } + const sessionObjectId = sessionResponse.results[0].objectId; + const userId = sessionResponse.results[0].user.objectId; + // Re-fetch the session with the caller's auth context so that + // protectedFields and CLP apply correctly + const userAuth = new Auth.Auth({ + config: req.config, + isMaster: false, + user: Parse.Object.fromJSON({ className: '_User', objectId: userId }), + installationId: req.info.installationId, + }); + const response = await rest.get( + req.config, + userAuth, + '_Session', + sessionObjectId, + {}, + req.info.clientSDK, + req.info.context + ); + if (!response.results || response.results.length == 0) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token not found.'); + } + return { + response: response.results[0], + }; } handleUpdateToRevocableSession(req) {