Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions spec/ParseSession.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 8 additions & 0 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading