From f537e677f02092c050abf4d930060a182492ca61 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:20:57 +0000 Subject: [PATCH 1/3] test: Fix flaky tests (#10320) --- spec/ParseGraphQLController.spec.js | 18 +++++++-- spec/ParseGraphQLServer.spec.js | 8 ++++ spec/rest.spec.js | 60 +++++++++++++---------------- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/spec/ParseGraphQLController.spec.js b/spec/ParseGraphQLController.spec.js index 9eed8f52be..15bbb48ab7 100644 --- a/spec/ParseGraphQLController.spec.js +++ b/spec/ParseGraphQLController.spec.js @@ -11,6 +11,8 @@ describe('ParseGraphQLController', () => { let databaseController; let cacheController; let databaseUpdateArgs; + let originalDbFind; + let originalDbUpdate; // Holds the graphQLConfig in memory instead of using the db let graphQLConfigRecord; @@ -34,17 +36,18 @@ describe('ParseGraphQLController', () => { databaseController = parseServer.config.databaseController; cacheController = parseServer.config.cacheController; - const defaultFind = databaseController.find.bind(databaseController); + originalDbFind = databaseController.find.bind(databaseController); + originalDbUpdate = databaseController.update.bind(databaseController); + databaseController.find = async (className, query, ...args) => { if (className === GraphQLConfigClassName && isEqual(query, { objectId: GraphQLConfigId })) { const graphQLConfigRecord = getConfigFromDb(); return graphQLConfigRecord ? [graphQLConfigRecord] : []; } else { - return defaultFind(className, query, ...args); + return originalDbFind(className, query, ...args); } }; - const defaultUpdate = databaseController.update.bind(databaseController); databaseController.update = async (className, query, update, fullQueryOptions) => { databaseUpdateArgs = [className, query, update, fullQueryOptions]; if ( @@ -57,13 +60,20 @@ describe('ParseGraphQLController', () => { ) { setConfigOnDb(update[GraphQLConfigKey]); } else { - return defaultUpdate(...databaseUpdateArgs); + return originalDbUpdate(...databaseUpdateArgs); } }; } databaseUpdateArgs = null; }); + afterAll(() => { + if (databaseController) { + databaseController.find = originalDbFind; + databaseController.update = originalDbUpdate; + } + }); + describe('constructor', () => { it('should require a databaseController', () => { expect(() => new ParseGraphQLController()).toThrow( diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 4ed104a013..1773fa263a 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -8658,6 +8658,13 @@ describe('ParseGraphQLServer', () => { }); describe('Data Types', () => { + beforeEach(async () => { + const schema = new Parse.Schema('SomeClass'); + await schema.purge().catch(() => {}); + await schema.delete().catch(() => {}); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + }); + it('should support String', async () => { try { const someFieldValue = 'some string'; @@ -10423,6 +10430,7 @@ describe('ParseGraphQLServer', () => { schema.addPointer('somePointerField', 'SomeClass'); schema.addRelation('someRelationField', 'SomeClass'); await schema.save(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const body = new FormData(); body.append( diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 063693b2bc..9416d9230e 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -653,7 +653,8 @@ describe('rest create', () => { password: 'zxcv', foo: 'bar', }; - const now = new Date(); + const defaultSessionLength = 1000 * 3600 * 24 * 365; + const before = Date.now(); rest .create(config, auth.nobody(config), '_User', user) @@ -670,10 +671,11 @@ describe('rest create', () => { expect(r.results.length).toEqual(1); const session = r.results[0]; - const actual = new Date(session.expiresAt.iso); - const expected = new Date(now.getTime() + 1000 * 3600 * 24 * 365); + const actual = new Date(session.expiresAt.iso).getTime(); + const after = Date.now(); - expect(Math.abs(actual - expected) <= jasmine.DEFAULT_TIMEOUT_INTERVAL).toEqual(true); + expect(actual).toBeGreaterThanOrEqual(before + defaultSessionLength); + expect(actual).toBeLessThanOrEqual(after + defaultSessionLength); done(); }); @@ -685,9 +687,9 @@ describe('rest create', () => { password: 'zxcv', foo: 'bar', }; - const sessionLength = 3600, // 1 Hour ahead - now = new Date(); // For reference later + const sessionLength = 3600; // 1 Hour ahead config.sessionLength = sessionLength; + const before = Date.now(); rest .create(config, auth.nobody(config), '_User', user) @@ -704,10 +706,11 @@ describe('rest create', () => { expect(r.results.length).toEqual(1); const session = r.results[0]; - const actual = new Date(session.expiresAt.iso); - const expected = new Date(now.getTime() + sessionLength * 1000); + const actual = new Date(session.expiresAt.iso).getTime(); + const after = Date.now(); - expect(Math.abs(actual - expected) <= jasmine.DEFAULT_TIMEOUT_INTERVAL).toEqual(true); + expect(actual).toBeGreaterThanOrEqual(before + sessionLength * 1000); + expect(actual).toBeLessThanOrEqual(after + sessionLength * 1000); done(); }) @@ -717,38 +720,27 @@ describe('rest create', () => { }); }); - it('can create a session with no expiration', done => { + it('can create a session with no expiration', async () => { + await reconfigureServer({ expireInactiveSessions: false }); + config = Config.get('test'); + const user = { username: 'asdf', password: 'zxcv', foo: 'bar', }; - config.expireInactiveSessions = false; - - rest - .create(config, auth.nobody(config), '_User', user) - .then(r => { - expect(Object.keys(r.response).length).toEqual(3); - expect(typeof r.response.objectId).toEqual('string'); - expect(typeof r.response.createdAt).toEqual('string'); - expect(typeof r.response.sessionToken).toEqual('string'); - return rest.find(config, auth.master(config), '_Session', { - sessionToken: r.response.sessionToken, - }); - }) - .then(r => { - expect(r.results.length).toEqual(1); - const session = r.results[0]; - expect(session.expiresAt).toBeUndefined(); + const r = await rest.create(config, auth.nobody(config), '_User', user); + expect(Object.keys(r.response).length).toEqual(3); + expect(typeof r.response.objectId).toEqual('string'); + expect(typeof r.response.createdAt).toEqual('string'); + expect(typeof r.response.sessionToken).toEqual('string'); - done(); - }) - .catch(err => { - console.error(err); - fail(err); - done(); - }); + const s = await rest.find(config, auth.master(config), '_Session', { + sessionToken: r.response.sessionToken, + }); + expect(s.results.length).toEqual(1); + expect(s.results[0].expiresAt).toBeUndefined(); }); it('can create object in volatileClasses if masterKey', done => { From 770be8647424d92f5425c41fa81065ffbbb171ed Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:35:44 +0000 Subject: [PATCH 2/3] fix: 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) --- spec/vulnerabilities.spec.js | 91 ++++++++++++++++++++++++++++++++++++ src/Routers/UsersRouter.js | 4 +- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index b0626bb001..9763ea77f2 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -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' }); + }); + }); }); diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7372ac6baa..cfd5f43bed 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -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 => { From 7fec0d0733746cd63d91b8d8c4f36dba9ab7be9d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 26 Mar 2026 20:37:08 +0000 Subject: [PATCH 3/3] chore(release): 9.7.0-alpha.7 [skip ci] # [9.7.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.7.0-alpha.6...9.7.0-alpha.7) (2026-03-26) ### Bug Fixes * 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](https://github.com/parse-community/parse-server/issues/10323)) ([770be86](https://github.com/parse-community/parse-server/commit/770be8647424d92f5425c41fa81065ffbbb171ed)) --- changelogs/CHANGELOG_alpha.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md index 83c83ee1ff..bf072feb63 100644 --- a/changelogs/CHANGELOG_alpha.md +++ b/changelogs/CHANGELOG_alpha.md @@ -1,3 +1,10 @@ +# [9.7.0-alpha.7](https://github.com/parse-community/parse-server/compare/9.7.0-alpha.6...9.7.0-alpha.7) (2026-03-26) + + +### Bug Fixes + +* 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](https://github.com/parse-community/parse-server/issues/10323)) ([770be86](https://github.com/parse-community/parse-server/commit/770be8647424d92f5425c41fa81065ffbbb171ed)) + # [9.7.0-alpha.6](https://github.com/parse-community/parse-server/compare/9.7.0-alpha.5...9.7.0-alpha.6) (2026-03-26) diff --git a/package-lock.json b/package-lock.json index 7ecadd9cfd..729df3578e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "9.7.0-alpha.6", + "version": "9.7.0-alpha.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "9.7.0-alpha.6", + "version": "9.7.0-alpha.7", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 501f82a191..5d7ce54565 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "9.7.0-alpha.6", + "version": "9.7.0-alpha.7", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": {