Parse Server: Auth data exposed via /users/me endpoint
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.61 and 9.6.0-alpha.55, an authenticated user calling GET /users/me receives unsanitized auth data, including sensitive credentials such as MFA TOTP secrets and recovery codes. The endpoint internally uses master-level authentication for the session query, and the master context leaks through to the user data, bypassing auth adapter sanitization. An attacker who obtains a user's session token can extract MFA secrets to generate valid TOTP codes indefinitely. This issue has been patched in versions 8.6.61 and 9.6.0-alpha.55.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | >= 9.0.0, < 9.6.0-alpha.55 | 9.6.0-alpha.55 |
parse-servernpm | < 8.6.61 | 8.6.61 |
Affected products
1- Range: < 8.6.61
Patches
2875cf10ac979fix: 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
7- github.com/advisories/GHSA-37mj-c2wf-cx96ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33627ghsaADVISORY
- github.com/parse-community/parse-server/commit/5b8998e6866bcf75be7b5bb625e27d23bfaf912cghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/875cf10ac979bd60f70e7a0c534e2bc194d6982fghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10278ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/10279ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-37mj-c2wf-cx96ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.