@@ -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' , ( ) => {
0 commit comments