Parse Server: Session update endpoint allows overwriting server-generated session fields
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 8.6.57 and 9.6.0-alpha.48, an authenticated user can overwrite server-generated session fields such as expiresAt and createdWith when updating their own session via the REST API. This allows bypassing the server's configured session lifetime policy, making a session effectively permanent. This issue has been patched in versions 8.6.57 and 9.6.0-alpha.48.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.6.0-alpha.48 | 9.6.0-alpha.48 |
parse-servernpm | < 8.6.57 | 8.6.57 |
Affected products
1- Range: < 8.6.57
Patches
226b628c8fb3cfix: Session update endpoint allows overwriting server-generated session fields ([GHSA-jc39-686j-wp6q](https://github.com/parse-community/parse-server/security/advisories/GHSA-jc39-686j-wp6q)) (#10264)
2 files changed · +141 −0
spec/ParseSession.spec.js+137 −0 modified@@ -256,4 +256,141 @@ describe('Parse.Session', () => { expect(newSession.createdWith.action).toBe('create'); expect(newSession.createdWith.authProvider).toBeUndefined(); }); + + it('should reject expiresAt when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdateuser1', 'password'); + const sessionToken = user.getSessionToken(); + + // Get the session objectId + 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; + + // Attempt to overwrite expiresAt via PUT + 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: { __type: 'Date', iso: '2099-12-31T23:59:59.000Z' }, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + + // Verify expiresAt was not changed + 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 createdWith when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdateuser2', 'password'); + const sessionToken = user.getSessionToken(); + + // Get the session objectId + 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; + + // Attempt to overwrite createdWith via PUT + 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: { action: 'attacker', authProvider: 'evil' }, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + + // Verify createdWith was not changed + 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 allow master key to update expiresAt on a session', async () => { + const user = await Parse.User.signUp('sessionupdateuser3', 'password'); + const sessionToken = user.getSessionToken(); + + // Get the session objectId + 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 farFuture = '2099-12-31T23:59:59.000Z'; + + // Master key should be able to update expiresAt + await request({ + method: 'PUT', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + expiresAt: { __type: 'Date', iso: farFuture }, + }, + }); + + // Verify expiresAt was changed + const verifyRes = await request({ + method: 'GET', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(verifyRes.data.expiresAt.iso).toBe(farFuture); + }); });
src/RestWrite.js+4 −0 modified@@ -1147,6 +1147,10 @@ RestWrite.prototype.handleSession = function () { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } else if (this.data.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); + } else if (this.data.expiresAt && !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) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } if (!this.auth.isMaster) { this.query = {
ea68fc0b22a6fix: Session update endpoint allows overwriting server-generated session fields ([GHSA-jc39-686j-wp6q](https://github.com/parse-community/parse-server/security/advisories/GHSA-jc39-686j-wp6q)) (#10263)
2 files changed · +141 −0
spec/ParseSession.spec.js+137 −0 modified@@ -257,6 +257,143 @@ describe('Parse.Session', () => { expect(newSession.createdWith.authProvider).toBeUndefined(); }); + it('should reject expiresAt when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdateuser1', 'password'); + const sessionToken = user.getSessionToken(); + + // Get the session objectId + 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; + + // Attempt to overwrite expiresAt via PUT + 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: { __type: 'Date', iso: '2099-12-31T23:59:59.000Z' }, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + + // Verify expiresAt was not changed + 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 createdWith when updating a session via PUT', async () => { + const user = await Parse.User.signUp('sessionupdateuser2', 'password'); + const sessionToken = user.getSessionToken(); + + // Get the session objectId + 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; + + // Attempt to overwrite createdWith via PUT + 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: { action: 'attacker', authProvider: 'evil' }, + }, + }).catch(e => e); + + expect(updateRes.data.code).toBe(Parse.Error.INVALID_KEY_NAME); + + // Verify createdWith was not changed + 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 allow master key to update expiresAt on a session', async () => { + const user = await Parse.User.signUp('sessionupdateuser3', 'password'); + const sessionToken = user.getSessionToken(); + + // Get the session objectId + 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 farFuture = '2099-12-31T23:59:59.000Z'; + + // Master key should be able to update expiresAt + await request({ + method: 'PUT', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }, + body: { + expiresAt: { __type: 'Date', iso: farFuture }, + }, + }); + + // Verify expiresAt was changed + const verifyRes = await request({ + method: 'GET', + url: `http://localhost:8378/1/sessions/${sessionId}`, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }); + expect(verifyRes.data.expiresAt.iso).toBe(farFuture); + }); + describe('PUT /sessions/me', () => { it('should return error with invalid session token', async () => { const response = await request({
src/RestWrite.js+4 −0 modified@@ -1180,6 +1180,10 @@ RestWrite.prototype.handleSession = function () { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } else if (this.data.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); + } else if (this.data.expiresAt && !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) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } if (!this.auth.isMaster) { this.query = {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-jc39-686j-wp6qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33527ghsaADVISORY
- github.com/parse-community/parse-server/commit/26b628c8fb3cc79ea955374769eebcff6f8a8a73ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/ea68fc0b22a6056c9675149469ff57817f7cf984ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10263ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10264ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-jc39-686j-wp6qghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.