From 6c9021b54888d77ff571cb8758e786c2cac7dc3b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:58:39 +0000 Subject: [PATCH 1/7] fix --- spec/vulnerabilities.spec.js | 181 +++++++++++++++++++++++++++++++++++ src/Config.js | 7 ++ src/Options/Definitions.js | 6 ++ src/Options/docs.js | 1 + src/Options/index.js | 7 ++ src/Routers/UsersRouter.js | 8 ++ types/Options/index.d.ts | 1 + 7 files changed, 211 insertions(+) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index c38ad99fdf..38f0f319cb 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -1,4 +1,5 @@ const request = require('../lib/request'); +const Config = require('../lib/Config'); describe('Vulnerabilities', () => { describe('(GHSA-8xq9-g7ch-35hg) Custom object ID allows to acquire role privilege', () => { @@ -1704,3 +1705,183 @@ describe('(GHSA-r2m8-pxm9-9c4g) Protected fields WHERE clause bypass via dot-not expect(res.status).toBe(400); }); }); + +describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint', () => { + let sendVerificationEmail; + + async function createTestUsers() { + const user = new Parse.User(); + user.setUsername('testuser'); + user.setPassword('password123'); + user.set('email', 'unverified@example.com'); + await user.signUp(); + + const user2 = new Parse.User(); + user2.setUsername('verifieduser'); + user2.setPassword('password123'); + user2.set('email', 'verified@example.com'); + await user2.signUp(); + const config = Config.get(Parse.applicationId); + await config.database.update( + '_User', + { username: 'verifieduser' }, + { emailVerified: true } + ); + } + + describe('default (emailVerifySuccessOnInvalidEmail: true)', () => { + beforeEach(async () => { + sendVerificationEmail = jasmine.createSpy('sendVerificationEmail'); + await reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: true, + emailAdapter: { + sendVerificationEmail, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + }); + await createTestUsers(); + }); + it('returns success for non-existent email', async () => { + const response = await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'nonexistent@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + expect(response.status).toBe(200); + expect(response.data).toEqual({}); + }); + + it('returns success for already verified email', async () => { + const response = await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'verified@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + expect(response.status).toBe(200); + expect(response.data).toEqual({}); + }); + + it('returns success for unverified email', async () => { + const response = await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'unverified@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + expect(response.status).toBe(200); + expect(response.data).toEqual({}); + }); + + it('does not send verification email for non-existent email', async () => { + sendVerificationEmail.calls.reset(); + await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'nonexistent@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + expect(sendVerificationEmail).not.toHaveBeenCalled(); + }); + + it('does not send verification email for already verified email', async () => { + sendVerificationEmail.calls.reset(); + await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'verified@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + expect(sendVerificationEmail).not.toHaveBeenCalled(); + }); + }); + + describe('opt-out (emailVerifySuccessOnInvalidEmail: false)', () => { + beforeEach(async () => { + sendVerificationEmail = jasmine.createSpy('sendVerificationEmail'); + await reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: true, + emailVerifySuccessOnInvalidEmail: false, + emailAdapter: { + sendVerificationEmail, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + }); + await createTestUsers(); + }); + + it('returns error for non-existent email', async () => { + const response = await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'nonexistent@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }).catch(e => e); + expect(response.data.code).toBe(Parse.Error.EMAIL_NOT_FOUND); + }); + + it('returns error for already verified email', async () => { + const response = await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'verified@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }).catch(e => e); + expect(response.status).not.toBe(200); + }); + }); + + it('rejects invalid emailVerifySuccessOnInvalidEmail values', async () => { + const invalidValues = [[], {}, 1, 'string']; + for (const value of invalidValues) { + await expectAsync( + reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:8378/1', + verifyUserEmails: true, + emailVerifySuccessOnInvalidEmail: value, + emailAdapter: { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }, + }) + ).toBeRejectedWith('emailVerifySuccessOnInvalidEmail must be a boolean value'); + } + }); +}); diff --git a/src/Config.js b/src/Config.js index 920f72d1e6..8cc21090e4 100644 --- a/src/Config.js +++ b/src/Config.js @@ -200,6 +200,7 @@ export class Config { _publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, + emailVerifySuccessOnInvalidEmail, }) { const emailAdapter = userController.adapter; if (verifyUserEmails) { @@ -209,6 +210,7 @@ export class Config { publicServerURL: publicServerURL || _publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, + emailVerifySuccessOnInvalidEmail, }); } } @@ -462,6 +464,7 @@ export class Config { ) { throw 'resetPasswordSuccessOnInvalidEmail must be a boolean value'; } + } } @@ -504,6 +507,7 @@ export class Config { publicServerURL, emailVerifyTokenValidityDuration, emailVerifyTokenReuseIfValid, + emailVerifySuccessOnInvalidEmail, }) { if (!emailAdapter) { throw 'An emailAdapter is required for e-mail verification and password resets.'; @@ -525,6 +529,9 @@ export class Config { if (emailVerifyTokenReuseIfValid && !emailVerifyTokenValidityDuration) { throw 'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration'; } + if (emailVerifySuccessOnInvalidEmail && typeof emailVerifySuccessOnInvalidEmail !== 'boolean') { + throw 'emailVerifySuccessOnInvalidEmail must be a boolean value'; + } } static validateFileUploadOptions(fileUpload) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 1359e1b27f..2274bd96a8 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -197,6 +197,12 @@ module.exports.ParseServerOptions = { help: 'Adapter module for email sending', action: parsers.moduleOrObjectParser, }, + emailVerifySuccessOnInvalidEmail: { + env: 'PARSE_SERVER_EMAIL_VERIFY_SUCCESS_ON_INVALID_EMAIL', + help: 'Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.
Requires option `verifyUserEmails: true`.', + action: parsers.booleanParser, + default: true, + }, emailVerifyTokenReuseIfValid: { env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', help: 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`.', diff --git a/src/Options/docs.js b/src/Options/docs.js index 54a49f5fb8..166f3cf33f 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -39,6 +39,7 @@ * @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending + * @property {Boolean} emailVerifySuccessOnInvalidEmail Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.
Requires option `verifyUserEmails: true`. * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {Number} emailVerifyTokenValidityDuration Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`. * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true diff --git a/src/Options/index.js b/src/Options/index.js index b8e239e37f..31b7b580e1 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -235,6 +235,13 @@ export interface ParseServerOptions { Requires option `verifyUserEmails: true`. :DEFAULT: false */ emailVerifyTokenReuseIfValid: ?boolean; + /* Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid. +

+ Default is `true`. +
+ Requires option `verifyUserEmails: true`. + :DEFAULT: true */ + emailVerifySuccessOnInvalidEmail: ?boolean; /* Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`. diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index edd1bdae4e..2ca2da2641 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -547,8 +547,13 @@ export class UsersRouter extends ClassesRouter { ); } + const verifyEmailSuccessOnInvalidEmail = req.config.emailVerifySuccessOnInvalidEmail ?? true; + const results = await req.config.database.find('_User', { email: email }, {}, Auth.maintenance(req.config)); if (!results.length || results.length < 1) { + if (verifyEmailSuccessOnInvalidEmail) { + return { response: {} }; + } throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`); } const user = results[0]; @@ -557,6 +562,9 @@ export class UsersRouter extends ClassesRouter { delete user.password; if (user.emailVerified) { + if (verifyEmailSuccessOnInvalidEmail) { + return { response: {} }; + } throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`); } diff --git a/types/Options/index.d.ts b/types/Options/index.d.ts index 49e58cc1df..6a8b1494ac 100644 --- a/types/Options/index.d.ts +++ b/types/Options/index.d.ts @@ -95,6 +95,7 @@ export interface ParseServerOptions { preventSignupWithUnverifiedEmail?: boolean; emailVerifyTokenValidityDuration?: number; emailVerifyTokenReuseIfValid?: boolean; + emailVerifySuccessOnInvalidEmail?: boolean; sendUserEmailVerification?: boolean | ((params: SendEmailVerificationRequest) => boolean | Promise); accountLockout?: AccountLockoutOptions; passwordPolicy?: PasswordPolicyOptions; From e94448b3bd27299e0955270877e6c000fd8c1f9c Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:03:10 +0000 Subject: [PATCH 2/7] sc --- spec/SecurityCheckGroups.spec.js | 8 +++++++ .../CheckGroups/CheckGroupServerConfig.js | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/spec/SecurityCheckGroups.spec.js b/spec/SecurityCheckGroups.spec.js index 696b7da640..354fed8b0e 100644 --- a/spec/SecurityCheckGroups.spec.js +++ b/spec/SecurityCheckGroups.spec.js @@ -49,6 +49,8 @@ describe('Security Check Groups', () => { expect(group.checks()[6].checkState()).toBe(CheckState.success); expect(group.checks()[8].checkState()).toBe(CheckState.success); expect(group.checks()[9].checkState()).toBe(CheckState.success); + expect(group.checks()[10].checkState()).toBe(CheckState.success); + expect(group.checks()[11].checkState()).toBe(CheckState.success); }); it('checks fail correctly', async () => { @@ -67,6 +69,10 @@ describe('Security Check Groups', () => { graphQLDepth: -1, graphQLFields: -1, }; + config.passwordPolicy = { + resetPasswordSuccessOnInvalidEmail: false, + }; + config.emailVerifySuccessOnInvalidEmail = false; await reconfigureServer(config); const group = new CheckGroupServerConfig(); @@ -79,6 +85,8 @@ describe('Security Check Groups', () => { expect(group.checks()[6].checkState()).toBe(CheckState.fail); expect(group.checks()[8].checkState()).toBe(CheckState.fail); expect(group.checks()[9].checkState()).toBe(CheckState.fail); + expect(group.checks()[10].checkState()).toBe(CheckState.fail); + expect(group.checks()[11].checkState()).toBe(CheckState.fail); }); it_only_db('mongo')('checks succeed correctly (MongoDB specific)', async () => { diff --git a/src/Security/CheckGroups/CheckGroupServerConfig.js b/src/Security/CheckGroups/CheckGroupServerConfig.js index d8d8e647e6..36b327dc91 100644 --- a/src/Security/CheckGroups/CheckGroupServerConfig.js +++ b/src/Security/CheckGroups/CheckGroupServerConfig.js @@ -151,6 +151,30 @@ class CheckGroupServerConfig extends CheckGroup { } }, }), + new Check({ + title: 'Password reset endpoint user enumeration mitigated', + warning: + 'The password reset endpoint returns distinct error responses for invalid email addresses, which allows attackers to enumerate registered users.', + solution: + "Change Parse Server configuration to 'passwordPolicy.resetPasswordSuccessOnInvalidEmail: true'.", + check: () => { + if (config.passwordPolicy?.resetPasswordSuccessOnInvalidEmail === false) { + throw 1; + } + }, + }), + new Check({ + title: 'Email verification endpoint user enumeration mitigated', + warning: + 'The email verification endpoint returns distinct error responses for invalid email addresses, which allows attackers to enumerate registered users.', + solution: + "Change Parse Server configuration to 'emailVerifySuccessOnInvalidEmail: true'.", + check: () => { + if (config.emailVerifySuccessOnInvalidEmail === false) { + throw 1; + } + }, + }), new Check({ title: 'LiveQuery regex timeout enabled', warning: From 94de7fa713daa1572bd17ad70151ccb0b4ea0a49 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:27:16 +0000 Subject: [PATCH 3/7] Update vulnerabilities.spec.js --- spec/vulnerabilities.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 38f0f319cb..2096872374 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -1775,6 +1775,7 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint }); it('returns success for unverified email', async () => { + sendVerificationEmail.calls.reset(); const response = await request({ url: 'http://localhost:8378/1/verificationEmailRequest', method: 'POST', @@ -1787,6 +1788,8 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint }); expect(response.status).toBe(200); expect(response.data).toEqual({}); + await jasmine.timeout(); + expect(sendVerificationEmail).toHaveBeenCalledTimes(1); }); it('does not send verification email for non-existent email', async () => { @@ -1864,6 +1867,22 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint }).catch(e => e); expect(response.status).not.toBe(200); }); + + it('sends verification email for unverified email', async () => { + sendVerificationEmail.calls.reset(); + await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { email: 'unverified@example.com' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + await jasmine.timeout(); + expect(sendVerificationEmail).toHaveBeenCalledTimes(1); + }); }); it('rejects invalid emailVerifySuccessOnInvalidEmail values', async () => { From ad97b18763d23f514554d0bbb7ce71824e8918e7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:31:15 +0000 Subject: [PATCH 4/7] Update EmailVerificationToken.spec.js --- spec/EmailVerificationToken.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 82114760af..11a901f399 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -1085,6 +1085,7 @@ describe('Email Verification Token Expiration:', () => { emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', + emailVerifySuccessOnInvalidEmail: false, }); user.setUsername('no_new_verification_token_once_verified'); user.setPassword('expiringToken'); @@ -1131,6 +1132,7 @@ describe('Email Verification Token Expiration:', () => { emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', + emailVerifySuccessOnInvalidEmail: false, }); const response = await request({ url: 'http://localhost:8378/1/verificationEmailRequest', From fddbd7e1af865cc39ed3a2164fc9bc0c5b93fb76 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:32:28 +0000 Subject: [PATCH 5/7] fix https://github.com/parse-community/parse-server/pull/10172#discussion_r2911289168 --- spec/vulnerabilities.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 2096872374..4503f3a7be 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -1865,7 +1865,8 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint 'Content-Type': 'application/json', }, }).catch(e => e); - expect(response.status).not.toBe(200); + expect(response.data.code).toBe(Parse.Error.OTHER_CAUSE); + expect(response.data.error).toBe('Email verified@example.com is already verified.'); }); it('sends verification email for unverified email', async () => { From 39f38e27efd6c7071721f1452b797b0be6e1dbc4 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:33:47 +0000 Subject: [PATCH 6/7] fix https://github.com/parse-community/parse-server/pull/10172#discussion_r2911289171 --- src/Options/Definitions.js | 2 +- src/Options/docs.js | 2 +- src/Options/index.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 2274bd96a8..672325c072 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -199,7 +199,7 @@ module.exports.ParseServerOptions = { }, emailVerifySuccessOnInvalidEmail: { env: 'PARSE_SERVER_EMAIL_VERIFY_SUCCESS_ON_INVALID_EMAIL', - help: 'Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.
Requires option `verifyUserEmails: true`.', + help: 'Set to `true` if a request to verify the email should return a success response even if the provided email address does not belong to a verifiable account, for example because it is unknown or already verified, or `false` if the request should return an error response in those cases.

Default is `true`.
Requires option `verifyUserEmails: true`.', action: parsers.booleanParser, default: true, }, diff --git a/src/Options/docs.js b/src/Options/docs.js index 166f3cf33f..e076f591aa 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -39,7 +39,7 @@ * @property {Boolean} directAccess Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

⚠️ In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`. * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending - * @property {Boolean} emailVerifySuccessOnInvalidEmail Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.
Requires option `verifyUserEmails: true`. + * @property {Boolean} emailVerifySuccessOnInvalidEmail Set to `true` if a request to verify the email should return a success response even if the provided email address does not belong to a verifiable account, for example because it is unknown or already verified, or `false` if the request should return an error response in those cases.

Default is `true`.
Requires option `verifyUserEmails: true`. * @property {Boolean} emailVerifyTokenReuseIfValid Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`. * @property {Number} emailVerifyTokenValidityDuration Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`. * @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true diff --git a/src/Options/index.js b/src/Options/index.js index 31b7b580e1..2d0c91f33a 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -235,7 +235,7 @@ export interface ParseServerOptions { Requires option `verifyUserEmails: true`. :DEFAULT: false */ emailVerifyTokenReuseIfValid: ?boolean; - /* Set to `true` if a request to verify the email should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid. + /* Set to `true` if a request to verify the email should return a success response even if the provided email address does not belong to a verifiable account, for example because it is unknown or already verified, or `false` if the request should return an error response in those cases.

Default is `true`.
From 6265f882f2ba7eaf45623318085ebd3c4372db4b Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:37:57 +0000 Subject: [PATCH 7/7] fix https://github.com/parse-community/parse-server/pull/10172#pullrequestreview-3921896932 --- spec/vulnerabilities.spec.js | 2 +- src/Config.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 4503f3a7be..cd780974b6 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -1887,7 +1887,7 @@ describe('(GHSA-w54v-hf9p-8856) User enumeration via email verification endpoint }); it('rejects invalid emailVerifySuccessOnInvalidEmail values', async () => { - const invalidValues = [[], {}, 1, 'string']; + const invalidValues = [[], {}, 0, 1, '', 'string']; for (const value of invalidValues) { await expectAsync( reconfigureServer({ diff --git a/src/Config.js b/src/Config.js index 8cc21090e4..c164594457 100644 --- a/src/Config.js +++ b/src/Config.js @@ -459,7 +459,7 @@ export class Config { } if ( - passwordPolicy.resetPasswordSuccessOnInvalidEmail && + passwordPolicy.resetPasswordSuccessOnInvalidEmail !== undefined && typeof passwordPolicy.resetPasswordSuccessOnInvalidEmail !== 'boolean' ) { throw 'resetPasswordSuccessOnInvalidEmail must be a boolean value'; @@ -529,7 +529,7 @@ export class Config { if (emailVerifyTokenReuseIfValid && !emailVerifyTokenValidityDuration) { throw 'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration'; } - if (emailVerifySuccessOnInvalidEmail && typeof emailVerifySuccessOnInvalidEmail !== 'boolean') { + if (emailVerifySuccessOnInvalidEmail !== undefined && typeof emailVerifySuccessOnInvalidEmail !== 'boolean') { throw 'emailVerifySuccessOnInvalidEmail must be a boolean value'; } }