CVE-2026-34215
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.63 and 9.7.0-alpha.7, the verify password endpoint returns unsanitized authentication data, including MFA TOTP secrets, recovery codes, and OAuth access tokens. An attacker who knows a user's password can extract the MFA secret to generate valid MFA codes, defeating multi-factor authentication protection. This issue has been patched in versions 8.6.63 and 9.7.0-alpha.7.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.7.0-alpha.7 | 9.7.0-alpha.7 |
parse-servernpm | < 8.6.63 | 8.6.63 |
Affected products
7cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*+ 6 more
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha1:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha2:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha3:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha4:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha5:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:9.7.0:alpha6:*:*:*:node.js:*:*
- cpe:2.3:a:parseplatform:parse-server:*:*:*:*:*:node.js:*:*range: <8.6.63
Patches
4770be8647424fix: Auth data exposed via verify password endpoint ([GHSA-wp76-gg32-8258](https://github.com/parse-community/parse-server/security/advisories/GHSA-wp76-gg32-8258)) (#10323)
2 files changed · +93 −2
spec/vulnerabilities.spec.js+91 −0 modified@@ -4600,4 +4600,95 @@ describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field n expect(meResponse.data.authData?.mfa).toEqual({ status: 'enabled' }); }); }); + + describe('(GHSA-wp76-gg32-8258) /verifyPassword leaks raw authData via missing afterFind', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }; + + it('does not leak raw MFA authData via /verifyPassword', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + verifyUserEmails: false, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + // Enable MFA + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken } + ); + // Verify MFA data is stored (master key) + await user.fetch({ useMasterKey: true }); + expect(user.get('authData').mfa.secret).toBe(secret.base32); + expect(user.get('authData').mfa.recovery).toBeDefined(); + // POST /verifyPassword should NOT include raw MFA data + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/verifyPassword', + body: JSON.stringify({ username: 'username', password: 'password' }), + }); + expect(response.data.authData?.mfa?.secret).toBeUndefined(); + expect(response.data.authData?.mfa?.recovery).toBeUndefined(); + expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); + + it('does not leak raw MFA authData via GET /verifyPassword', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + verifyUserEmails: false, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + await user.save( + { authData: { mfa: { secret: secret.base32, token: totp.generate() } } }, + { sessionToken } + ); + // GET /verifyPassword should NOT include raw MFA data + const response = await request({ + headers, + method: 'GET', + url: `http://localhost:8378/1/verifyPassword?username=username&password=password`, + }); + expect(response.data.authData?.mfa?.secret).toBeUndefined(); + expect(response.data.authData?.mfa?.recovery).toBeUndefined(); + expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); + }); });
src/Routers/UsersRouter.js+2 −2 modified@@ -422,10 +422,10 @@ export class UsersRouter extends ClassesRouter { handleVerifyPassword(req) { return this._authenticateUserFromRequest(req) - .then(user => { + .then(async user => { // Remove hidden properties. UsersRouter.removeHiddenProperties(user); - + await req.config.authDataManager.runAfterFind(req, user.authData); return { response: user }; }) .catch(error => {
a1d4e7b12a12fix: Auth data exposed via verify password endpoint ([GHSA-wp76-gg32-8258](https://github.com/parse-community/parse-server/security/advisories/GHSA-wp76-gg32-8258)) (#10324)
2 files changed · +93 −2
spec/vulnerabilities.spec.js+91 −0 modified@@ -4142,3 +4142,94 @@ describe('(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context' expect(meResponse.data.authData?.mfa).toEqual({ status: 'enabled' }); }); }); + +describe('(GHSA-wp76-gg32-8258) /verifyPassword leaks raw authData via missing afterFind', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }; + + it('does not leak raw MFA authData via /verifyPassword', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + verifyUserEmails: false, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + // Enable MFA + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken } + ); + // Verify MFA data is stored (master key) + await user.fetch({ useMasterKey: true }); + expect(user.get('authData').mfa.secret).toBe(secret.base32); + expect(user.get('authData').mfa.recovery).toBeDefined(); + // POST /verifyPassword should NOT include raw MFA data + const response = await request({ + headers, + method: 'POST', + url: 'http://localhost:8378/1/verifyPassword', + body: JSON.stringify({ username: 'username', password: 'password' }), + }); + expect(response.data.authData?.mfa?.secret).toBeUndefined(); + expect(response.data.authData?.mfa?.recovery).toBeUndefined(); + expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); + + it('does not leak raw MFA authData via GET /verifyPassword', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + verifyUserEmails: false, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + await user.save( + { authData: { mfa: { secret: secret.base32, token: totp.generate() } } }, + { sessionToken } + ); + // GET /verifyPassword should NOT include raw MFA data + const response = await request({ + headers, + method: 'GET', + url: `http://localhost:8378/1/verifyPassword?username=username&password=password`, + }); + expect(response.data.authData?.mfa?.secret).toBeUndefined(); + expect(response.data.authData?.mfa?.recovery).toBeUndefined(); + expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); +});
src/Routers/UsersRouter.js+2 −2 modified@@ -422,10 +422,10 @@ export class UsersRouter extends ClassesRouter { handleVerifyPassword(req) { return this._authenticateUserFromRequest(req) - .then(user => { + .then(async user => { // Remove hidden properties. UsersRouter.removeHiddenProperties(user); - + await req.config.authDataManager.runAfterFind(req, user.authData); return { response: user }; }) .catch(error => {
875cf10ac979fix: Auth data exposed via /users/me endpoint ([GHSA-37mj-c2wf-cx96](https://github.com/parse-community/parse-server/security/advisories/GHSA-37mj-c2wf-cx96)) (#10278)
2 files changed · +143 −24
spec/vulnerabilities.spec.js+103 −0 modified@@ -4497,4 +4497,107 @@ describe('(GHSA-p2w6-rmh7-w8q3) SQL Injection via aggregate and distinct field n expect(response.data?.results).toEqual(['Alice']); }); }); + + describe('(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }; + + it('does not leak raw MFA authData via /users/me', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + // Enable MFA + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken } + ); + // Verify MFA data is stored (master key) + await user.fetch({ useMasterKey: true }); + expect(user.get('authData').mfa.secret).toBe(secret.base32); + expect(user.get('authData').mfa.recovery).toBeDefined(); + // GET /users/me should NOT include raw MFA data + const response = await request({ + headers: { + ...headers, + 'X-Parse-Session-Token': sessionToken, + }, + method: 'GET', + url: 'http://localhost:8378/1/users/me', + }); + expect(response.data.authData?.mfa?.secret).toBeUndefined(); + expect(response.data.authData?.mfa?.recovery).toBeUndefined(); + expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); + + it('returns same authData from /users/me and /users/:id', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + await user.save( + { authData: { mfa: { secret: secret.base32, token: totp.generate() } } }, + { sessionToken } + ); + // Fetch via /users/me + const meResponse = await request({ + headers: { + ...headers, + 'X-Parse-Session-Token': sessionToken, + }, + method: 'GET', + url: 'http://localhost:8378/1/users/me', + }); + // Fetch via /users/:id + const idResponse = await request({ + headers: { + ...headers, + 'X-Parse-Session-Token': sessionToken, + }, + method: 'GET', + url: `http://localhost:8378/1/users/${user.id}`, + }); + // Both should return the same sanitized authData + expect(meResponse.data.authData).toEqual(idResponse.data.authData); + expect(meResponse.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); + }); });
src/Routers/UsersRouter.js+40 −24 modified@@ -176,34 +176,50 @@ export class UsersRouter extends ClassesRouter { }); } - handleMe(req) { + async handleMe(req) { if (!req.info || !req.info.sessionToken) { throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); } const sessionToken = req.info.sessionToken; - return rest - .find( - req.config, - Auth.master(req.config), - '_Session', - { sessionToken }, - { include: 'user' }, - req.info.clientSDK, - req.info.context - ) - .then(response => { - if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); - } else { - const user = response.results[0].user; - // Send token back on the login, because SDKs expect that. - user.sessionToken = sessionToken; - - // Remove hidden properties. - UsersRouter.removeHiddenProperties(user); - return { response: user }; - } - }); + // Query the session with master key to validate the session token, + // but do NOT include 'user' to avoid leaking user data via master context + const sessionResponse = await rest.find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken }, + {}, + req.info.clientSDK, + req.info.context + ); + if ( + !sessionResponse.results || + sessionResponse.results.length == 0 || + !sessionResponse.results[0].user + ) { + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); + } + const userId = sessionResponse.results[0].user.objectId; + // Re-fetch the user with the caller's auth context so that + // protectedFields, CLP, and auth adapter afterFind apply correctly + const userResponse = await rest.get( + req.config, + req.auth, + '_User', + userId, + {}, + req.info.clientSDK, + req.info.context + ); + if (!userResponse.results || userResponse.results.length == 0) { + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); + } + const user = userResponse.results[0]; + // Send token back on the login, because SDKs expect that. + user.sessionToken = sessionToken; + // Remove hidden properties. + UsersRouter.removeHiddenProperties(user); + return { response: user }; } async handleLogIn(req) {
5b8998e6866bfix: Auth data exposed via /users/me endpoint ([GHSA-37mj-c2wf-cx96](https://github.com/parse-community/parse-server/security/advisories/GHSA-37mj-c2wf-cx96)) (#10279)
2 files changed · +143 −24
spec/vulnerabilities.spec.js+103 −0 modified@@ -4039,3 +4039,106 @@ describe('(GHSA-2299-ghjr-6vjp) MFA recovery code reuse via concurrent requests' expect(remainingRecovery).not.toContain(recoveryCode); }); }); + +describe('(GHSA-37mj-c2wf-cx96) /users/me leaks raw authData via master context', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }; + + it('does not leak raw MFA authData via /users/me', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + const token = totp.generate(); + // Enable MFA + await user.save( + { authData: { mfa: { secret: secret.base32, token } } }, + { sessionToken } + ); + // Verify MFA data is stored (master key) + await user.fetch({ useMasterKey: true }); + expect(user.get('authData').mfa.secret).toBe(secret.base32); + expect(user.get('authData').mfa.recovery).toBeDefined(); + // GET /users/me should NOT include raw MFA data + const response = await request({ + headers: { + ...headers, + 'X-Parse-Session-Token': sessionToken, + }, + method: 'GET', + url: 'http://localhost:8378/1/users/me', + }); + expect(response.data.authData?.mfa?.secret).toBeUndefined(); + expect(response.data.authData?.mfa?.recovery).toBeUndefined(); + expect(response.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); + + it('returns same authData from /users/me and /users/:id', async () => { + await reconfigureServer({ + auth: { + mfa: { + enabled: true, + options: ['TOTP'], + algorithm: 'SHA1', + digits: 6, + period: 30, + }, + }, + }); + const user = await Parse.User.signUp('username', 'password'); + const sessionToken = user.getSessionToken(); + const OTPAuth = require('otpauth'); + const secret = new OTPAuth.Secret(); + const totp = new OTPAuth.TOTP({ + algorithm: 'SHA1', + digits: 6, + period: 30, + secret, + }); + await user.save( + { authData: { mfa: { secret: secret.base32, token: totp.generate() } } }, + { sessionToken } + ); + // Fetch via /users/me + const meResponse = await request({ + headers: { + ...headers, + 'X-Parse-Session-Token': sessionToken, + }, + method: 'GET', + url: 'http://localhost:8378/1/users/me', + }); + // Fetch via /users/:id + const idResponse = await request({ + headers: { + ...headers, + 'X-Parse-Session-Token': sessionToken, + }, + method: 'GET', + url: `http://localhost:8378/1/users/${user.id}`, + }); + // Both should return the same sanitized authData + expect(meResponse.data.authData).toEqual(idResponse.data.authData); + expect(meResponse.data.authData?.mfa).toEqual({ status: 'enabled' }); + }); +});
src/Routers/UsersRouter.js+40 −24 modified@@ -170,34 +170,50 @@ export class UsersRouter extends ClassesRouter { }); } - handleMe(req) { + async handleMe(req) { if (!req.info || !req.info.sessionToken) { throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); } const sessionToken = req.info.sessionToken; - return rest - .find( - req.config, - Auth.master(req.config), - '_Session', - { sessionToken }, - { include: 'user' }, - req.info.clientSDK, - req.info.context - ) - .then(response => { - if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); - } else { - const user = response.results[0].user; - // Send token back on the login, because SDKs expect that. - user.sessionToken = sessionToken; - - // Remove hidden properties. - UsersRouter.removeHiddenProperties(user); - return { response: user }; - } - }); + // Query the session with master key to validate the session token, + // but do NOT include 'user' to avoid leaking user data via master context + const sessionResponse = await rest.find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken }, + {}, + req.info.clientSDK, + req.info.context + ); + if ( + !sessionResponse.results || + sessionResponse.results.length == 0 || + !sessionResponse.results[0].user + ) { + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); + } + const userId = sessionResponse.results[0].user.objectId; + // Re-fetch the user with the caller's auth context so that + // protectedFields, CLP, and auth adapter afterFind apply correctly + const userResponse = await rest.get( + req.config, + req.auth, + '_User', + userId, + {}, + req.info.clientSDK, + req.info.context + ); + if (!userResponse.results || userResponse.results.length == 0) { + throw createSanitizedError(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token', req.config); + } + const user = userResponse.results[0]; + // Send token back on the login, because SDKs expect that. + user.sessionToken = sessionToken; + // Remove hidden properties. + UsersRouter.removeHiddenProperties(user); + return { response: user }; } async handleLogIn(req) {
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
11- github.com/parse-community/parse-server/commit/770be8647424d92f5425c41fa81065ffbbb171ednvdPatchWEB
- github.com/parse-community/parse-server/commit/a1d4e7b12a12f16d3870dbee582a36765858e94cnvdPatchWEB
- github.com/parse-community/parse-server/pull/10323nvdIssue TrackingPatchWEB
- github.com/parse-community/parse-server/pull/10324nvdIssue TrackingPatchWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-wp76-gg32-8258nvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-wp76-gg32-8258ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34215ghsaADVISORY
- github.com/parse-community/parse-server/commit/5b8998e6866bcf75be7b5bb625e27d23bfaf912cghsaWEB
- github.com/parse-community/parse-server/commit/875cf10ac979bd60f70e7a0c534e2bc194d6982fghsaWEB
- github.com/parse-community/parse-server/pull/10278ghsaWEB
- github.com/parse-community/parse-server/pull/10279ghsaWEB
News mentions
0No linked articles in our index yet.