From 718b5aa279e98ccd6389bffba62c6f3d4888ab61 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:12:52 +0200 Subject: [PATCH] fix: Creating a session can delete another user's session --- spec/ParseSession.spec.js | 65 +++++++++++++++++++++++++++++++++++++++ src/RestWrite.js | 8 +++++ 2 files changed, 73 insertions(+) diff --git a/spec/ParseSession.spec.js b/spec/ParseSession.spec.js index b622bb04c0..ca3c727f26 100644 --- a/spec/ParseSession.spec.js +++ b/spec/ParseSession.spec.js @@ -257,6 +257,71 @@ describe('Parse.Session', () => { expect(newSession.createdWith.authProvider).toBeUndefined(); }); + it('does not delete another user\'s session when creating a session via POST /classes/_Session', async () => { + const victim = await Parse.User.signUp('dedupvictim', 'password'); + const attacker = await Parse.User.signUp('dedupattacker', 'password'); + const victimId = victim.id; + const installationId = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'; + + // Victim logs in on a known installation, creating a session with that installationId. + const victimLogin = await request({ + method: 'POST', + url: 'http://localhost:8378/1/login', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': installationId, + 'Content-Type': 'application/json', + }, + body: { username: 'dedupvictim', password: 'password' }, + }); + const victimSessionToken = victimLogin.data.sessionToken; + + // Another user creates a session while naming the victim as `user` and supplying + // the victim's installationId. The session dedup must not delete the victim's session. + await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/_Session', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': attacker.getSessionToken(), + 'Content-Type': 'application/json', + }, + body: { + user: { __type: 'Pointer', className: '_User', objectId: victimId }, + installationId, + sessionToken: 'r:someothertoken', + }, + }); + + // The victim's session on that installation must still exist... + const sessions = await request({ + method: 'GET', + url: 'http://localhost:8378/1/classes/_Session', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + const victimSession = sessions.data.results.find( + s => s.installationId === installationId && s.user && s.user.objectId === victimId + ); + expect(victimSession).toBeDefined(); + + // ...and the victim's session token must still authenticate. + const meResponse = await request({ + method: 'GET', + url: 'http://localhost:8378/1/users/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': victimSessionToken, + }, + }); + expect(meResponse.data.objectId).toBe(victimId); + }); + it('should reject expiresAt when updating a session via PUT', async () => { const user = await Parse.User.signUp('sessionupdateuser1', 'password'); const sessionToken = user.getSessionToken(); diff --git a/src/RestWrite.js b/src/RestWrite.js index 98c9fd4656..5c6f3fdeee 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1151,6 +1151,14 @@ RestWrite.prototype.deleteEmailResetTokenIfNeeded = function () { }; RestWrite.prototype.destroyDuplicatedSessions = function () { + // Skip if the response is already set, matching the other write-pipeline steps + // (runDatabaseOperation, runAfterSaveTrigger). A non-master POST /classes/_Session + // create has handleSession() set this.response before this runs, so this guard + // prevents the dedup delete from acting on the client-supplied `user`/`installationId` + // rather than on the server-generated session data. + if (this.response) { + return; + } // Only for _Session, and at creation time if (this.className != '_Session' || this.query) { return;