Skip to content

Commit 0df8779

Browse files
authored
fix: Creating a session can delete another user's session (#10582)
1 parent d76845f commit 0df8779

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

spec/ParseSession.spec.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,71 @@ describe('Parse.Session', () => {
257257
expect(newSession.createdWith.authProvider).toBeUndefined();
258258
});
259259

260+
it('does not delete another user\'s session when creating a session via POST /classes/_Session', async () => {
261+
const victim = await Parse.User.signUp('dedupvictim', 'password');
262+
const attacker = await Parse.User.signUp('dedupattacker', 'password');
263+
const victimId = victim.id;
264+
const installationId = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d';
265+
266+
// Victim logs in on a known installation, creating a session with that installationId.
267+
const victimLogin = await request({
268+
method: 'POST',
269+
url: 'http://localhost:8378/1/login',
270+
headers: {
271+
'X-Parse-Application-Id': 'test',
272+
'X-Parse-REST-API-Key': 'rest',
273+
'X-Parse-Installation-Id': installationId,
274+
'Content-Type': 'application/json',
275+
},
276+
body: { username: 'dedupvictim', password: 'password' },
277+
});
278+
const victimSessionToken = victimLogin.data.sessionToken;
279+
280+
// Another user creates a session while naming the victim as `user` and supplying
281+
// the victim's installationId. The session dedup must not delete the victim's session.
282+
await request({
283+
method: 'POST',
284+
url: 'http://localhost:8378/1/classes/_Session',
285+
headers: {
286+
'X-Parse-Application-Id': 'test',
287+
'X-Parse-REST-API-Key': 'rest',
288+
'X-Parse-Session-Token': attacker.getSessionToken(),
289+
'Content-Type': 'application/json',
290+
},
291+
body: {
292+
user: { __type: 'Pointer', className: '_User', objectId: victimId },
293+
installationId,
294+
sessionToken: 'r:someothertoken',
295+
},
296+
});
297+
298+
// The victim's session on that installation must still exist...
299+
const sessions = await request({
300+
method: 'GET',
301+
url: 'http://localhost:8378/1/classes/_Session',
302+
headers: {
303+
'X-Parse-Application-Id': 'test',
304+
'X-Parse-Master-Key': 'test',
305+
},
306+
});
307+
const victimSession = sessions.data.results.find(
308+
s => s.installationId === installationId && s.user && s.user.objectId === victimId
309+
);
310+
expect(victimSession).toBeDefined();
311+
312+
// ...and the victim's session token must still authenticate.
313+
const meResponse = await request({
314+
method: 'GET',
315+
url: 'http://localhost:8378/1/users/me',
316+
headers: {
317+
'X-Parse-Application-Id': 'test',
318+
'X-Parse-REST-API-Key': 'rest',
319+
'X-Parse-Session-Token': victimSessionToken,
320+
},
321+
});
322+
expect(meResponse.data.objectId).toBe(victimId);
323+
});
324+
260325
it('should reject expiresAt when updating a session via PUT', async () => {
261326
const user = await Parse.User.signUp('sessionupdateuser1', 'password');
262327
const sessionToken = user.getSessionToken();

src/RestWrite.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,14 @@ RestWrite.prototype.deleteEmailResetTokenIfNeeded = function () {
11511151
};
11521152

11531153
RestWrite.prototype.destroyDuplicatedSessions = function () {
1154+
// Skip if the response is already set, matching the other write-pipeline steps
1155+
// (runDatabaseOperation, runAfterSaveTrigger). A non-master POST /classes/_Session
1156+
// create has handleSession() set this.response before this runs, so this guard
1157+
// prevents the dedup delete from acting on the client-supplied `user`/`installationId`
1158+
// rather than on the server-generated session data.
1159+
if (this.response) {
1160+
return;
1161+
}
11541162
// Only for _Session, and at creation time
11551163
if (this.className != '_Session' || this.query) {
11561164
return;

0 commit comments

Comments
 (0)