Skip to content

Commit 8444c5a

Browse files
committed
fix: Email verification resend page leaks user existence
The Pages route for resend_verification_email redirected to different pages on success vs failure, allowing user enumeration. Now respects the emailVerifySuccessOnInvalidEmail option (default true) to always redirect to the success page, matching the API route behavior.
1 parent 80db912 commit 8444c5a

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

spec/PagesRouter.spec.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,8 +840,10 @@ describe('Pages Router', () => {
840840
followRedirects: false,
841841
});
842842
expect(formResponse.status).toEqual(303);
843+
// With emailVerifySuccessOnInvalidEmail: true (default), the resend
844+
// page always redirects to the success page to prevent user enumeration
843845
expect(formResponse.text).toContain(
844-
`/${locale}/${pages.emailVerificationSendFail.defaultFile}`
846+
`/${locale}/${pages.emailVerificationSendSuccess.defaultFile}`
845847
);
846848
});
847849

@@ -1040,6 +1042,81 @@ describe('Pages Router', () => {
10401042
}).catch(e => e);
10411043
expect(response.status).not.toBe(500);
10421044
});
1045+
1046+
it('does not leak email verification status via resend page when emailVerifySuccessOnInvalidEmail is true', async () => {
1047+
const emailAdapter = {
1048+
sendVerificationEmail: () => {},
1049+
sendPasswordResetEmail: () => {},
1050+
sendMail: () => {},
1051+
};
1052+
await reconfigureServer({
1053+
...config,
1054+
verifyUserEmails: true,
1055+
emailVerifySuccessOnInvalidEmail: true,
1056+
emailAdapter,
1057+
});
1058+
1059+
// Create a user with unverified email
1060+
const user = new Parse.User();
1061+
user.setUsername('realuser');
1062+
user.setPassword('password123');
1063+
user.setEmail('real@example.com');
1064+
await user.signUp();
1065+
1066+
const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`;
1067+
1068+
// Resend for existing unverified user
1069+
const existingResponse = await request({
1070+
method: 'POST',
1071+
url: formUrl,
1072+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1073+
body: 'username=realuser',
1074+
followRedirects: false,
1075+
}).catch(e => e);
1076+
1077+
// Resend for non-existing user
1078+
const nonExistingResponse = await request({
1079+
method: 'POST',
1080+
url: formUrl,
1081+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1082+
body: 'username=fakeuser',
1083+
followRedirects: false,
1084+
}).catch(e => e);
1085+
1086+
// Both should redirect to the same page (success) to prevent enumeration
1087+
expect(existingResponse.status).toBe(303);
1088+
expect(nonExistingResponse.status).toBe(303);
1089+
expect(existingResponse.headers.location).toContain('email_verification_send_success');
1090+
expect(nonExistingResponse.headers.location).toContain('email_verification_send_success');
1091+
});
1092+
1093+
it('does leak email verification status via resend page when emailVerifySuccessOnInvalidEmail is false', async () => {
1094+
const emailAdapter = {
1095+
sendVerificationEmail: () => {},
1096+
sendPasswordResetEmail: () => {},
1097+
sendMail: () => {},
1098+
};
1099+
await reconfigureServer({
1100+
...config,
1101+
verifyUserEmails: true,
1102+
emailVerifySuccessOnInvalidEmail: false,
1103+
emailAdapter,
1104+
});
1105+
1106+
const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`;
1107+
1108+
// Resend for non-existing user should redirect to fail page
1109+
const nonExistingResponse = await request({
1110+
method: 'POST',
1111+
url: formUrl,
1112+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1113+
body: 'username=fakeuser',
1114+
followRedirects: false,
1115+
}).catch(e => e);
1116+
1117+
expect(nonExistingResponse.status).toBe(303);
1118+
expect(nonExistingResponse.headers.location).toContain('email_verification_send_fail');
1119+
});
10431120
});
10441121

10451122
describe('custom route', () => {

src/Routers/PagesRouter.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,16 @@ export class PagesRouter extends PromiseRouter {
120120
}
121121

122122
const userController = config.userController;
123+
const suppressError = config.emailVerifySuccessOnInvalidEmail ?? true;
123124

124125
return userController.resendVerificationEmail(username, req, token).then(
125126
() => {
126127
return this.goToPage(req, pages.emailVerificationSendSuccess);
127128
},
128129
() => {
130+
if (suppressError) {
131+
return this.goToPage(req, pages.emailVerificationSendSuccess);
132+
}
129133
return this.goToPage(req, pages.emailVerificationSendFail);
130134
}
131135
);

0 commit comments

Comments
 (0)