From ebccd7fe2708007e62f705ee1c820a6766178777 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 29 Mar 2026 04:56:09 +0100 Subject: [PATCH] fix: Session field immutability bypass via falsy-value guard ([GHSA-f6j3-w9v3-cq22](https://github.com/parse-community/parse-server/security/advisories/GHSA-f6j3-w9v3-cq22)) (#10348) --- spec/ParseSession.spec.js | 152 ++++++++++++++++++++++++++++++++++++++ src/RestWrite.js | 8 +- 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/spec/ParseSession.spec.js b/spec/ParseSession.spec.js index a76c96860..345c9e6d5 100644 --- a/spec/ParseSession.spec.js +++ b/spec/ParseSession.spec.js @@ -393,4 +393,156 @@ describe('Parse.Session', () => { }); expect(verifyRes.data.expiresAt.iso).toBe(farFuture); }); + + it('should reject null expiresAt when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdatenull1', 'password'); + const sessionToken = user.getSessionToken(); + + const sessionRes = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + }); + const sessionId = sessionRes.data.objectId; + const originalExpiresAt = sessionRes.data.expiresAt; + + const updateRes = await request({ + method: 'PUT', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + 'Content-Type': 'application/json', + }, + body: { + expiresAt: null, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + + const verifyRes = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + }); + expect(verifyRes.data.expiresAt).toEqual(originalExpiresAt); + }); + + it('should reject null createdWith when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdatenull2', 'password'); + const sessionToken = user.getSessionToken(); + + const sessionRes = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + }); + const sessionId = sessionRes.data.objectId; + const originalCreatedWith = sessionRes.data.createdWith; + + const updateRes = await request({ + method: 'PUT', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + 'Content-Type': 'application/json', + }, + body: { + createdWith: null, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + + const verifyRes = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + }); + expect(verifyRes.data.createdWith).toEqual(originalCreatedWith); + }); + + it('should reject null installationId when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdatenull3', 'password'); + const sessionToken = user.getSessionToken(); + + const sessionRes = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + }); + const sessionId = sessionRes.data.objectId; + + const updateRes = await request({ + method: 'PUT', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + 'Content-Type': 'application/json', + }, + body: { + installationId: null, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + }); + + it('should reject null sessionToken when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdatenull4', 'password'); + const sessionToken = user.getSessionToken(); + + const sessionRes = await request({ + method: 'GET', + url: 'http://localhost:8378/1/sessions/me', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + }); + const sessionId = sessionRes.data.objectId; + + const updateRes = await request({ + method: 'PUT', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + 'Content-Type': 'application/json', + }, + body: { + sessionToken: null, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 630cc7f26..2d167731b 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1174,13 +1174,13 @@ RestWrite.prototype.handleSession = function () { if (this.query) { if (this.data.user && !this.auth.isMaster && this.data.user.objectId != this.auth.user.id) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); - } else if (this.data.installationId) { + } else if ('installationId' in this.data) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); - } else if (this.data.sessionToken) { + } else if ('sessionToken' in this.data) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); - } else if (this.data.expiresAt && !this.auth.isMaster && !this.auth.isMaintenance) { + } else if ('expiresAt' in this.data && !this.auth.isMaster && !this.auth.isMaintenance) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); - } else if (this.data.createdWith && !this.auth.isMaster && !this.auth.isMaintenance) { + } else if ('createdWith' in this.data && !this.auth.isMaster && !this.auth.isMaintenance) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } if (!this.auth.isMaster) {