CVE-2022-23619: Weak Password Recovery Mechanism for Forgotten Password (CWE-640)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. In affected versions it's possible to guess if a user has an account on the wiki by using the "Forgot your password" form, even if the wiki is closed to guest users. This problem has been patched on XWiki 12.10.9, 13.4.1 and 13.6RC1. Users are advised yo update. There are no known workarounds for this issue.
CWE description: The product contains a mechanism for users to recover or change their passwords without knowing the original password, but the mechanism is weak.
CVSS v3 base score: 7.5
Published: 2022-02-09T21:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/pom.xml, xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-docker/src/test/it/org/xwiki/administration/test/ui/ResetPasswordIT.java, xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-pageobjects/src/main/java/org/xwiki/administration/test/po/ResetPasswordPage.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties, xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-api/src/main/java/org/xwiki/security/authentication/ResetPasswordRequestResponse.java, xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManager.java, xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordRequestResponse.java, xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/test/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManagerTest.java, xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/main/java/org/xwiki/security/authentication/script/AuthenticationScriptService.java, xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/test/java/org/xwiki/security/authentication/script/AuthenticationScriptServiceTest.java, xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/resetpasswordinline.vm
Commit date: 2021-06-29 09:48:38 +0200
Commit message: XWIKI-18787: Authentication API does not return proper results (#1651)

  * Refactor a bit the Authentication API to be more sound
  * Fix related tests
  * Fix missing escape
Diff: diff --git a/xwiki-platform-core/pom.xml b/xwiki-platform-core/pom.xml
index 9996c61587e8..dc68469fb65a 100644
--- a/xwiki-platform-core/pom.xml
+++ b/xwiki-platform-core/pom.xml
@@ -154,10 +154,28 @@
                        <criticality>highlight</criticality>
                      </item>
                    </differences>
                  </revapi.differences>
             -->
+            <revapi.differences>
+              <criticality>highlight</criticality>
+              <differences>
+                <item>
+                  <ignore>true</ignore>
+                  <code>java.method.returnTypeChanged</code>
+                  <old>method javax.mail.internet.InternetAddress org.xwiki.security.authentication.script.AuthenticationScriptService::requestResetPassword(org.xwiki.user.UserReference) throws org.xwiki.security.authentication.ResetPasswordException</old>
+                  <new>method void org.xwiki.security.authentication.script.AuthenticationScriptService::requestResetPassword(org.xwiki.user.UserReference) throws org.xwiki.security.authentication.ResetPasswordException</new>
+                  <justification>Unstable API: this API was not properly designed and shouldn't have returned that in first place.</justification>
+                </item>
+                <item>
+                  <ignore>true</ignore>
+                  <code>java.method.removed</code>
+                  <old>method javax.mail.internet.InternetAddress org.xwiki.security.authentication.ResetPasswordRequestResponse::getUserEmail()</old>
+                  <justification>Unstable API: this API was not properly designed and shouldn't have returned that in first place.</justification>
+                </item>
+              </differences>
+            </revapi.differences>
           </analysisConfiguration>
         </configuration>
       </plugin>
     </plugins>
   </build>
diff --git a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-docker/src/test/it/org/xwiki/administration/test/ui/ResetPasswordIT.java b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-docker/src/test/it/org/xwiki/administration/test/ui/ResetPasswordIT.java
index e3f095fa50da..dbd295f46381 100644
--- a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-docker/src/test/it/org/xwiki/administration/test/ui/ResetPasswordIT.java
+++ b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-docker/src/test/it/org/xwiki/administration/test/ui/ResetPasswordIT.java
@@ -115,21 +115,23 @@ public void resetForgottenPassword(TestUtils setup) throws Exception
         ResetPasswordPage resetPasswordPage = ResetPasswordPage.gotoPage();
 
         // Try to reset the password of a non existent user
         resetPasswordPage.setUserName("SomeUserThatDoesNotExist");
         resetPasswordPage = resetPasswordPage.clickResetPassword();
-        assertFalse(resetPasswordPage.isResetPasswordSent());
-        assertTrue(resetPasswordPage.getMessage().contains("user does not exist"));
+
+        // there should not have any indication if the user exists or not.
+        assertTrue(resetPasswordPage.isFormSubmitted());
 
         // Try again
-        resetPasswordPage = resetPasswordPage.clickRetry();
+        resetPasswordPage = ResetPasswordPage.gotoPage();
 
         // Try to reset the password of our user, when he has no email set
         resetPasswordPage.setUserName(userName);
         resetPasswordPage.clickResetPassword();
-        assertFalse(resetPasswordPage.isResetPasswordSent());
-        assertTrue(resetPasswordPage.getMessage().contains("email address not provided"));
+
+        // there should not have any indication if an email address is provided or not.
+        assertTrue(resetPasswordPage.isFormSubmitted());
 
         // Try again. This time, set the user's email address in the profile
         setup.loginAsSuperAdmin();
         setup.updateObject("XWiki", userName, "XWiki.XWikiUsers", 0, "email", "foo@bar.com", "form_token",
             setup.getSecretToken());
@@ -143,11 +145,11 @@ public void resetForgottenPassword(TestUtils setup) throws Exception
             "Actual message: " + newResetPasswordPage.getMessage());
         assertFalse(newResetPasswordPage.getMessage().contains("foo@bar.com"),
             "Actual message: " + newResetPasswordPage.getMessage());
 
         // Check the result
-        assertTrue(resetPasswordPage.isResetPasswordSent());
+        assertTrue(resetPasswordPage.isFormSubmitted());
         // Check the emails received by the user
         assertTrue(this.mail.waitForIncomingEmail(1));
         MimeMessage[] receivedEmails = this.mail.getReceivedMessages();
         assertEquals(1, receivedEmails.length);
         MimeMessage receivedEmail = receivedEmails[0];
diff --git a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-pageobjects/src/main/java/org/xwiki/administration/test/po/ResetPasswordPage.java b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-pageobjects/src/main/java/org/xwiki/administration/test/po/ResetPasswordPage.java
index 8b9afc47e980..6797d46774fe 100644
--- a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-pageobjects/src/main/java/org/xwiki/administration/test/po/ResetPasswordPage.java
+++ b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-pageobjects/src/main/java/org/xwiki/administration/test/po/ResetPasswordPage.java
@@ -65,11 +65,17 @@ public ResetPasswordPage clickResetPassword()
     {
         resetPasswordButton.click();
         return new ResetPasswordPage();
     }
 
-    public boolean isResetPasswordSent()
+    /**
+     * This method only checks if the form was properly submitted and didn't return an error.
+     * It does not mean that an email was necessarily sent.
+     *
+     * @return {@code true} if the form is properly submitted.
+     */
+    public boolean isFormSubmitted()
     {
         // If there is no form and we see an info box, then the request was sent.
         return !getDriver().hasElementWithoutWaiting(By.cssSelector("#resetPasswordForm"))
             && messageBox.getText().contains("An e-mail was sent to");
     }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
index 9d9f904b2d71..64ea27634ddf 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties
@@ -2447,11 +2447,11 @@ xe.admin.passwordReset.loginMessage=Forgot your password?
 
 xe.admin.passwordReset.title=Forgot your password?
 xe.admin.passwordReset.instructions=Please enter your username to start the password reset process.
 xe.admin.passwordReset.username.label=Username
 xe.admin.passwordReset.submit=Reset password
-xe.admin.passwordReset.emailSent=An e-mail was sent to {0}. Please follow the instructions in that e-mail to complete the password reset procedure.
+xe.admin.passwordReset.emailSentToUsername=An e-mail was sent to the address configured for user "{0}". Please follow the instructions in that e-mail to complete the password reset procedure.
 xe.admin.passwordReset.login=Login \u00BB
 xe.admin.passwordReset.error.noUser=The {0} user does not exist.
 xe.admin.passwordReset.error.ldapUser=The {0} user is an LDAP user. In that case the password has to be changed on the LDAP server.
 xe.admin.passwordReset.error.noEmail=Cannot reset password: email address not provided in the user profile.
 xe.admin.passwordReset.error.emailFailed=An unknown problem occurred while sending the reset email.
@@ -5499,10 +5499,15 @@ core.widgets.groupPicker.noResults=Group not found
 
 xe.admin.forgotUsername.result=Your username is: {0}
 xe.admin.forgotUsername.multipleResults=The following usernames are registered with this email address:
 xe.admin.forgotUsername.error.noAccount=No account is registered using this email address.
 
+#######################################
+## until 12.10.9, 13.6RC1, 13.4.1
+#######################################
+xe.admin.passwordReset.emailSent=An e-mail was sent to {0}. Please follow the instructions in that e-mail to complete the password reset procedure.
+
 ## Used to indicate where deprecated keys end
 #@deprecatedend
 
 ###############################################################################
 ## Old but critical deprecated
diff --git a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-api/src/main/java/org/xwiki/security/authentication/ResetPasswordRequestResponse.java b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-api/src/main/java/org/xwiki/security/authentication/ResetPasswordRequestResponse.java
index cbd22d2b2f87..14c17f202cb3 100644
--- a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-api/src/main/java/org/xwiki/security/authentication/ResetPasswordRequestResponse.java
+++ b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-api/src/main/java/org/xwiki/security/authentication/ResetPasswordRequestResponse.java
@@ -17,12 +17,10 @@
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.security.authentication;
 
-import javax.mail.internet.InternetAddress;
-
 import org.xwiki.stability.Unstable;
 import org.xwiki.user.UserReference;
 
 /**
  * Dedicated response interface for the first reset password request.
@@ -36,15 +34,10 @@
     /**
      * @return the reference of the user for whom the reset password request have been performed.
      */
     UserReference getUserReference();
 
-    /**
-     * @return the email address of the user for whom the reset password request have been performed.
-     */
-    InternetAddress getUserEmail();
-
     /**
      * @return the verification code to be send to the user.
      */
     String getVerificationCode();
 }
diff --git a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManager.java b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManager.java
index f1a69bc28ad1..73f73d672c36 100644
--- a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManager.java
+++ b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManager.java
@@ -27,10 +27,11 @@
 import javax.inject.Provider;
 import javax.inject.Singleton;
 import javax.mail.internet.InternetAddress;
 
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.localization.ContextualLocalizationManager;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.LocalDocumentReference;
 import org.xwiki.resource.ResourceReference;
@@ -108,182 +109,188 @@ public class DefaultResetPasswordManager implements ResetPasswordManager
     private UserReferenceSerializer<String> referenceSerializer;
 
     @Inject
     private Provider<ResetPasswordMailSender> resetPasswordMailSenderProvider;
 
-    private void checkUserReference(UserReference userReference) throws ResetPasswordException
-    {
-        if (!this.userManager.exists(userReference)) {
-            String exceptionMessage =
-                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
-                    userReference.toString());
-            throw new ResetPasswordException(exceptionMessage);
-        }
+    @Inject
+    private Logger logger;
 
+    private boolean checkUserReference(UserReference userReference) throws ResetPasswordException
+    {
         // FIXME: This check shouldn't be needed if we'd have the proper API to determine which kind of
         // authentication is used.
         if (!(userReference instanceof DocumentUserReference)) {
             throw new ResetPasswordException("Only user having a page on the wiki can reset their password.");
         }
+
+        return this.userManager.exists(userReference);
     }
 
     @Override
     public ResetPasswordRequestResponse requestResetPassword(UserReference userReference) throws ResetPasswordException
     {
-        this.checkUserReference(userReference);
-
-        UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
-        InternetAddress email = userProperties.getEmail();
-
-        if (email == null) {
-            String exceptionMessage =
-                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noEmail");
-            throw new ResetPasswordException(exceptionMessage);
-        }
-
-        DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
-        DocumentReference reference = documentUserReference.getReference();
-        XWikiContext context = this.contextProvider.get();
-
-        try {
-            XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
-
-            if (userDocument.getXObject(LDAP_CLASS_REFERENCE) != null) {
-                String exceptionMessage =
-                    this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.ldapUser",
-                        userReference.toString());
-                throw new ResetPasswordException(exceptionMessage);
+        if (this.checkUserReference(userReference)) {
+            UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
+            InternetAddress email = userProperties.getEmail();
+
+            if (email != null) {
+                DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
+                DocumentReference reference = documentUserReference.getReference();
+                XWikiContext context = this.contextProvider.get();
+
+                try {
+                    XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
+
+                    if (userDocument.getXObject(LDAP_CLASS_REFERENCE) != null) {
+                        String exceptionMessage =
+                            this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.ldapUser",
+                                userReference.toString());
+                        throw new ResetPasswordException(exceptionMessage);
+                    }
+
+                    BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE, true, context);
+                    String verificationCode = context.getWiki().generateRandomString(30);
+                    xObject.set(VERIFICATION_PROPERTY, verificationCode, context);
+
+                    String saveComment =
+                        this.localizationManager.getTranslationPlain("xe.admin.passwordReset.versionComment");
+                    context.getWiki().saveDocument(userDocument, saveComment, true, context);
+
+                    return new DefaultResetPasswordRequestResponse(userReference, verificationCode);
+                } catch (XWikiException e) {
+                    throw new ResetPasswordException(
+                        "Error when reading user document to perform reset password request.",
+                        e);
+                }
+            } else {
+                // In case the mail is not configured, we log a message to the admin.
+                this.logger.info("User [{}] asked to reset their password, but did not have any email configured.",
+                    userReference);
             }
-
-            BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE, true, context);
-            String verificationCode = context.getWiki().generateRandomString(30);
-            xObject.set(VERIFICATION_PROPERTY, verificationCode, context);
-
-            String saveComment =
-                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.versionComment");
-            context.getWiki().saveDocument(userDocument, saveComment, true, context);
-
-            return new DefaultResetPasswordRequestResponse(userReference, email, verificationCode);
-        } catch (XWikiException e) {
-            throw new ResetPasswordException("Error when reading user document to perform reset password request.", e);
         }
+        return new DefaultResetPasswordRequestResponse(userReference, null);
     }
 
     @Override
     public void sendResetPasswordEmailRequest(ResetPasswordRequestResponse requestResponse)
         throws ResetPasswordException
     {
-        AuthenticationResourceReference resourceReference =
-            new AuthenticationResourceReference(AuthenticationAction.RESET_PASSWORD);
-
-        UserReference userReference = requestResponse.getUserReference();
-        UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
-        String serializedUserReference = this.referenceSerializer.serialize(userReference);
-
-        // FIXME: this should be provided as part of the User API.
-        String formattedName = "";
-        if (!StringUtils.isBlank(userProperties.getFirstName())) {
-            formattedName += userProperties.getFirstName();
-        }
-        if (!StringUtils.isBlank(userProperties.getLastName())) {
-            if (!StringUtils.isBlank(formattedName)) {
-                formattedName += " ";
+        if (this.checkUserReference(requestResponse.getUserReference())) {
+            AuthenticationResourceReference resourceReference =
+                new AuthenticationResourceReference(AuthenticationAction.RESET_PASSWORD);
+
+            UserReference userReference = requestResponse.getUserReference();
+            UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
+            InternetAddress email = userProperties.getEmail();
+            String serializedUserReference = this.referenceSerializer.serialize(userReference);
+
+            // FIXME: this should be provided as part of the User API.
+            String formattedName = "";
+            if (!StringUtils.isBlank(userProperties.getFirstName())) {
+                formattedName += userProperties.getFirstName();
+            }
+            if (!StringUtils.isBlank(userProperties.getLastName())) {
+                if (!StringUtils.isBlank(formattedName)) {
+                    formattedName += " ";
+                }
+                formattedName += userProperties.getLastName();
+            }
+            if (StringUtils.isBlank(formattedName)) {
+                formattedName = serializedUserReference;
+            }
+            resourceReference.addParameter("u", serializedUserReference);
+            resourceReference.addParameter("v", requestResponse.getVerificationCode());
+
+            XWikiContext context = contextProvider.get();
+
+            ExtendedURL extendedURL = null;
+            try {
+                extendedURL = this.resourceReferenceSerializer.serialize(resourceReference);
+                extendedURL = this.urlNormalizer.normalize(extendedURL);
+                URL serverURL = context.getURLFactory().getServerURL(context);
+                URL externalVerificationURL = new URL(serverURL, extendedURL.serialize());
+
+                this.resetPasswordMailSenderProvider.get()
+                    .sendResetPasswordEmail(formattedName, email, externalVerificationURL);
+            } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException
+                | MalformedURLException e) {
+                throw new ResetPasswordException("Error when processing information for creating the email.", e);
             }
-            formattedName += userProperties.getLastName();
-        }
-        if (StringUtils.isBlank(formattedName)) {
-            formattedName = serializedUserReference;
-        }
-        resourceReference.addParameter("u", serializedUserReference);
-        resourceReference.addParameter("v", requestResponse.getVerificationCode());
-
-        XWikiContext context = contextProvider.get();
-
-        ExtendedURL extendedURL = null;
-        try {
-            extendedURL = this.resourceReferenceSerializer.serialize(resourceReference);
-            extendedURL = this.urlNormalizer.normalize(extendedURL);
-            URL serverURL = context.getURLFactory().getServerURL(context);
-            URL externalVerificationURL = new URL(serverURL, extendedURL.serialize());
-
-            this.resetPasswordMailSenderProvider.get()
-                .sendResetPasswordEmail(formattedName, requestResponse.getUserEmail(), externalVerificationURL);
-        } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException | MalformedURLException e)
-        {
-            throw new ResetPasswordException("Error when processing information for creating the email.", e);
         }
     }
 
     @Override
     public ResetPasswordRequestResponse checkVerificationCode(UserReference userReference, String verificationCode)
         throws ResetPasswordException
     {
-        this.checkUserReference(userReference);
-        XWikiContext context = this.contextProvider.get();
-
-        UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
-        InternetAddress email = userProperties.getEmail();
-
-        DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
-        DocumentReference reference = documentUserReference.getReference();
-        String exceptionMessage =
-            this.localizationManager.getTranslationPlain("xe.admin.passwordReset.step2.error.wrongParameters",
-                userReference.toString());
-
-        try {
-            XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
-            BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
-            if (xObject == null) {
-                throw new ResetPasswordException(exceptionMessage);
-            }
+        if (this.checkUserReference(userReference)) {
+            XWikiContext context = this.contextProvider.get();
 
-            String storedVerificationCode = xObject.getStringValue(VERIFICATION_PROPERTY);
-            BaseClass xClass = xObject.getXClass(context);
-            PropertyInterface verification = xClass.get(VERIFICATION_PROPERTY);
-            if (!(verification instanceof PasswordClass)) {
-                throw new ResetPasswordException("Bad definition of ResetPassword XClass.");
-            }
-            PasswordClass passwordClass = (PasswordClass) verification;
-            String equivalentPassword =
-                passwordClass.getEquivalentPassword(storedVerificationCode, verificationCode);
-
-            // We ensure to reset the verification code before checking if it's correct to avoid any bruteforce attack.
-            String newVerificationCode = context.getWiki().generateRandomString(30);
-            xObject.set(VERIFICATION_PROPERTY, newVerificationCode, context);
-            String saveComment = this.localizationManager
-                .getTranslationPlain("xe.admin.passwordReset.step2.versionComment.changeValidationKey");
-            context.getWiki().saveDocument(userDocument, saveComment, true, context);
-
-            if (!storedVerificationCode.equals(equivalentPassword)) {
-                throw new ResetPasswordException(exceptionMessage);
-            }
+            DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
+            DocumentReference reference = documentUserReference.getReference();
+            String exceptionMessage =
+                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.step2.error.wrongParameters",
+                    userReference.toString());
 
-            return new DefaultResetPasswordRequestResponse(userReference, email, newVerificationCode);
-        } catch (XWikiException e) {
-            throw new ResetPasswordException("Cannot open user document to check verification code.", e);
+            try {
+                XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
+                BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
+                if (xObject == null) {
+                    throw new ResetPasswordException(exceptionMessage);
+                }
+
+                String storedVerificationCode = xObject.getStringValue(VERIFICATION_PROPERTY);
+                BaseClass xClass = xObject.getXClass(context);
+                PropertyInterface verification = xClass.get(VERIFICATION_PROPERTY);
+                if (!(verification instanceof PasswordClass)) {
+                    throw new ResetPasswordException("Bad definition of ResetPassword XClass.");
+                }
+                PasswordClass passwordClass = (PasswordClass) verification;
+                String equivalentPassword =
+                    passwordClass.getEquivalentPassword(storedVerificationCode, verificationCode);
+
+                // We ensure to reset the verification code before checking if it's correct to avoid
+                // any bruteforce attack.
+                String newVerificationCode = context.getWiki().generateRandomString(30);
+                xObject.set(VERIFICATION_PROPERTY, newVerificationCode, context);
+                String saveComment = this.localizationManager
+                    .getTranslationPlain("xe.admin.passwordReset.step2.versionComment.changeValidationKey");
+                context.getWiki().saveDocument(userDocument, saveComment, true, context);
+
+                if (!storedVerificationCode.equals(equivalentPassword)) {
+                    throw new ResetPasswordException(exceptionMessage);
+                }
+
+                return new DefaultResetPasswordRequestResponse(userReference, newVerificationCode);
+            } catch (XWikiException e) {
+                throw new ResetPasswordException("Cannot open user document to check verification code.", e);
+            }
+        } else {
+            return new DefaultResetPasswordRequestResponse(userReference, null);
         }
     }
 
     @Override
     public void resetPassword(UserReference userReference, String newPassword)
         throws ResetPasswordException
     {
-        this.checkUserReference(userReference);
-        XWikiContext context = this.contextProvider.get();
-
-        DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
-        DocumentReference reference = documentUserReference.getReference();
-
-        try {
-            XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
-            userDocument.removeXObjects(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
-            BaseObject userXObject = userDocument.getXObject(USER_CLASS_REFERENCE);
-            userXObject.setStringValue("password", newPassword);
-
-            String saveComment = this.localizationManager.getTranslationPlain(
-                "xe.admin.passwordReset.step2.versionComment.passwordReset");
-            context.getWiki().saveDocument(userDocument, saveComment, true, context);
-        } catch (XWikiException e) {
-            throw new ResetPasswordException("Cannot open user document to perform reset password.", e);
+        if (this.checkUserReference(userReference)) {
+            XWikiContext context = this.contextProvider.get();
+
+            DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
+            DocumentReference reference = documentUserReference.getReference();
+
+            try {
+                XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
+                userDocument.removeXObjects(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
+                BaseObject userXObject = userDocument.getXObject(USER_CLASS_REFERENCE);
+                userXObject.setStringValue("password", newPassword);
+
+                String saveComment = this.localizationManager.getTranslationPlain(
+                    "xe.admin.passwordReset.step2.versionComment.passwordReset");
+                context.getWiki().saveDocument(userDocument, saveComment, true, context);
+            } catch (XWikiException e) {
+                throw new ResetPasswordException("Cannot open user document to perform reset password.", e);
+            }
         }
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordRequestResponse.java b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordRequestResponse.java
index ce72abe46ef4..8491ce422efb 100644
--- a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordRequestResponse.java
+++ b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/main/java/org/xwiki/security/authentication/internal/DefaultResetPasswordRequestResponse.java
@@ -17,12 +17,10 @@
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.security.authentication.internal;
 
-import javax.mail.internet.InternetAddress;
-
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.xwiki.security.authentication.ResetPasswordRequestResponse;
 import org.xwiki.user.UserReference;
 
@@ -33,23 +31,20 @@
  * @since 13.1RC1
  */
 public final class DefaultResetPasswordRequestResponse implements ResetPasswordRequestResponse
 {
     private final UserReference userReference;
-    private final InternetAddress userEmail;
     private final String verificationCode;
 
     /**
      * Default constructor.
      * @param reference the user for whom a reset password request is performed.
-     * @param userEmail the email of the user.
      * @param verificationCode the code to send for resetting the password.
      */
-    DefaultResetPasswordRequestResponse(UserReference reference, InternetAddress userEmail, String verificationCode)
+    DefaultResetPasswordRequestResponse(UserReference reference, String verificationCode)
     {
         this.userReference = reference;
-        this.userEmail = userEmail;
         this.verificationCode = verificationCode;
     }
 
     /**
      * @return the user for whom a reset password request is performed.
@@ -57,18 +52,10 @@ public final class DefaultResetPasswordRequestResponse implements ResetPasswordR
     public UserReference getUserReference()
     {
         return userReference;
     }
 
-    /**
-     * @return the email of the user.
-     */
-    public InternetAddress getUserEmail()
-    {
-        return userEmail;
-    }
-
     /**
      * @return the code to send for resetting the password.
      */
     public String getVerificationCode()
     {
@@ -88,20 +75,18 @@ public boolean equals(Object o)
 
         DefaultResetPasswordRequestResponse that = (DefaultResetPasswordRequestResponse) o;
 
         return new EqualsBuilder()
             .append(userReference, that.userReference)
-            .append(userEmail, that.userEmail)
             .append(verificationCode, that.verificationCode)
             .isEquals();
     }
 
     @Override
     public int hashCode()
     {
         return new HashCodeBuilder(17, 37)
             .append(userReference)
-            .append(userEmail)
             .append(verificationCode)
             .toHashCode();
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/test/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManagerTest.java b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/test/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManagerTest.java
index 58af27c09422..9d4f2079eee0 100644
--- a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/test/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManagerTest.java
+++ b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-default/src/test/java/org/xwiki/security/authentication/internal/DefaultResetPasswordManagerTest.java
@@ -26,19 +26,22 @@
 import javax.inject.Provider;
 import javax.mail.internet.InternetAddress;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 import org.xwiki.localization.ContextualLocalizationManager;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.resource.ResourceReference;
 import org.xwiki.resource.ResourceReferenceSerializer;
 import org.xwiki.security.authentication.AuthenticationAction;
 import org.xwiki.security.authentication.AuthenticationResourceReference;
 import org.xwiki.security.authentication.ResetPasswordException;
 import org.xwiki.security.authentication.ResetPasswordManager;
 import org.xwiki.security.authentication.ResetPasswordRequestResponse;
+import org.xwiki.test.LogLevel;
+import org.xwiki.test.junit5.LogCaptureExtension;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.url.ExtendedURL;
 import org.xwiki.url.URLNormalizer;
@@ -57,11 +60,15 @@
 import com.xpn.xwiki.objects.classes.PasswordClass;
 import com.xpn.xwiki.web.XWikiURLFactory;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /**
  * Tests for {@link DefaultResetPasswordManager}.
@@ -98,10 +105,13 @@
     private UserReferenceSerializer<String> referenceSerializer;
 
     @MockComponent
     private Provider<ResetPasswordMailSender> resetPasswordMailSenderProvider;
 
+    @RegisterExtension
+    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.INFO);
+
     private ResetPasswordMailSender resetPasswordMailSender;
 
     private DocumentUserReference userReference;
     private DocumentReference userDocumentReference;
     private UserProperties userProperties;
@@ -143,28 +153,23 @@ void requestResetPassword() throws Exception
         when(this.xWiki.generateRandomString(30)).thenReturn(verificationCode);
         when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.versionComment"))
             .thenReturn("Save verification code 42");
 
         ResetPasswordRequestResponse expectedResult =
-            new DefaultResetPasswordRequestResponse(this.userReference, email,
-                verificationCode);
+            new DefaultResetPasswordRequestResponse(this.userReference, verificationCode);
         assertEquals(expectedResult, this.resetPasswordManager.requestResetPassword(this.userReference));
         verify(xObject).set(DefaultResetPasswordManager.VERIFICATION_PROPERTY, verificationCode, context);
         verify(this.xWiki).saveDocument(this.userDocument, "Save verification code 42", true, this.context);
     }
 
     @Test
-    void requestResetPasswordUnexistingUser()
+    void requestResetPasswordUnexistingUser() throws ResetPasswordException
     {
         when(this.userReference.toString()).thenReturn("user:Foobar");
         when(this.userManager.exists(this.userReference)).thenReturn(false);
-        String exceptionMessage = "User [user:Foobar] doesn't exist";
-        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
-            "user:Foobar")).thenReturn(exceptionMessage);
-        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
-            () -> this.resetPasswordManager.requestResetPassword(this.userReference));
-        assertEquals(exceptionMessage, resetPasswordException.getMessage());
+        assertEquals(new DefaultResetPasswordRequestResponse(this.userReference, null),
+            this.resetPasswordManager.requestResetPassword(this.userReference));
     }
 
     @Test
     void requestResetPasswordNotDocumentReferenceUser()
     {
@@ -178,16 +183,15 @@ void requestResetPasswordNotDocumentReferenceUser()
 
     @Test
     void requestResetPasswordNoEmail() throws Exception
     {
         when(this.userManager.exists(this.userReference)).thenReturn(true);
-        String exceptionMessage = "User has no email address.";
-        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noEmail"))
-            .thenReturn(exceptionMessage);
-        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
-            () -> this.resetPasswordManager.requestResetPassword(this.userReference));
-        assertEquals(exceptionMessage, resetPasswordException.getMessage());
+        assertEquals(new DefaultResetPasswordRequestResponse(this.userReference, null),
+            this.resetPasswordManager.requestResetPassword(this.userReference));
+        when(this.userReference.toString()).thenReturn("foo");
+        assertEquals("User [foo] asked to reset their password, but did not have any email configured.",
+            logCapture.getMessage(0));
     }
 
     @Test
     void requestResetPasswordLdapUser() throws Exception
     {
@@ -208,10 +212,11 @@ void requestResetPasswordLdapUser() throws Exception
     }
 
     @Test
     void sendResetPasswordEmailRequest() throws Exception
     {
+        when(this.userManager.exists(this.userReference)).thenReturn(true);
         when(this.referenceSerializer.serialize(this.userReference)).thenReturn("user:Foobar");
         when(this.userProperties.getFirstName()).thenReturn("Foo");
         when(this.userProperties.getLastName()).thenReturn("Bar");
 
         AuthenticationResourceReference resourceReference =
@@ -229,13 +234,14 @@ void sendResetPasswordEmailRequest() throws Exception
         XWikiURLFactory urlFactory = mock(XWikiURLFactory.class);
         when(this.context.getURLFactory()).thenReturn(urlFactory);
         when(urlFactory.getServerURL(this.context)).thenReturn(new URL("http://xwiki.org"));
 
         InternetAddress email = new InternetAddress("foobar@xwiki.org");
+        when(this.userProperties.getEmail()).thenReturn(email);
+
         DefaultResetPasswordRequestResponse requestResponse =
-            new DefaultResetPasswordRequestResponse(this.userReference, email,
-                verificationCode);
+            new DefaultResetPasswordRequestResponse(this.userReference, verificationCode);
         this.resetPasswordManager.sendResetPasswordEmailRequest(requestResponse);
         verify(this.resetPasswordMailSender).sendResetPasswordEmail("Foo Bar", email,
             new URL("http://xwiki.org/xwiki/authenticate/reset?u=user%3AFoobar&v=foobar4242"));
     }
 
@@ -264,28 +270,24 @@ void checkVerificationCode() throws Exception
         String saveComment = "Save new verification code";
         when(this.localizationManager
             .getTranslationPlain("xe.admin.passwordReset.step2.versionComment.changeValidationKey"))
             .thenReturn(saveComment);
         DefaultResetPasswordRequestResponse expected =
-            new DefaultResetPasswordRequestResponse(this.userReference, email,
-                newVerificationCode);
+            new DefaultResetPasswordRequestResponse(this.userReference, newVerificationCode);
 
         assertEquals(expected, this.resetPasswordManager.checkVerificationCode(this.userReference, verificationCode));
         verify(this.xWiki).saveDocument(this.userDocument, saveComment, true, context);
     }
 
     @Test
-    void checkVerificationCodeUnexistingUser()
+    void checkVerificationCodeUnexistingUser() throws ResetPasswordException
     {
         when(this.userReference.toString()).thenReturn("user:Foobar");
         when(this.userManager.exists(this.userReference)).thenReturn(false);
-        String exceptionMessage = "User [user:Foobar] doesn't exist";
-        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
-            "user:Foobar")).thenReturn(exceptionMessage);
-        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
-            () -> this.resetPasswordManager.checkVerificationCode(this.userReference, "some code"));
-        assertEquals(exceptionMessage, resetPasswordException.getMessage());
+        ResetPasswordRequestResponse resetPasswordRequestResponse =
+            this.resetPasswordManager.checkVerificationCode(this.userReference, "some code");
+        assertEquals(new DefaultResetPasswordRequestResponse(this.userReference, null), resetPasswordRequestResponse);
     }
 
     @Test
     void checkVerificationCodeWrongCode() throws Exception
     {
@@ -356,16 +358,14 @@ void resetPassword() throws Exception
     @Test
     void resetPasswordUnexistingUser() throws Exception
     {
         when(this.userReference.toString()).thenReturn("user:Foobar");
         when(this.userManager.exists(this.userReference)).thenReturn(false);
-        String exceptionMessage = "User [user:Foobar] doesn't exist";
-        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
-            "user:Foobar")).thenReturn(exceptionMessage);
-        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
-            () -> this.resetPasswordManager.resetPassword(this.userReference, "some password"));
-        assertEquals(exceptionMessage, resetPasswordException.getMessage());
+        this.resetPasswordManager.resetPassword(this.userReference, "some password");
+        verify(this.xWiki, never()).getDocument(any(DocumentReference.class), any(XWikiContext.class));
+        verify(this.xWiki, never()).saveDocument(any(XWikiDocument.class), anyString(), anyBoolean(),
+            any(XWikiContext.class));
     }
 
     @Test
     void resetPasswordNotDocumentReferenceUser() throws Exception
     {
diff --git a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/main/java/org/xwiki/security/authentication/script/AuthenticationScriptService.java b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/main/java/org/xwiki/security/authentication/script/AuthenticationScriptService.java
index 05bcd9cd99c8..5c16d4bb5789 100644
--- a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/main/java/org/xwiki/security/authentication/script/AuthenticationScriptService.java
+++ b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/main/java/org/xwiki/security/authentication/script/AuthenticationScriptService.java
@@ -25,11 +25,10 @@
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 import javax.inject.Singleton;
-import javax.mail.internet.InternetAddress;
 
 import org.slf4j.Logger;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
@@ -188,25 +187,20 @@ public String getAuthenticationURL(String action, Map<String, Object> params)
      * Request a password reset for the given user.
      * This will result in computing a verification code and sending the appropriate link by email to the user.
      * This method returns the email address used, so that we can display it to the user.
      *
      * @param user the user for which to perform a reset password request.
-     * @return the email address used to send the verification code or {@code null} if the user calling this method
-     *         doesn't have programming rights.
      * @throws ResetPasswordException if any error occurs for performing the reset password request.
      * @since 13.1RC1
      */
     @Unstable
-    public InternetAddress requestResetPassword(UserReference user) throws ResetPasswordException
+    public void requestResetPassword(UserReference user) throws ResetPasswordException
     {
         if (this.authorizationManager.hasAccess(Right.PROGRAM)) {
             ResetPasswordRequestResponse resetPasswordRequestResponse =
                 this.resetPasswordManager.requestResetPassword(user);
             this.resetPasswordManager.sendResetPasswordEmailRequest(resetPasswordRequestResponse);
-            return resetPasswordRequestResponse.getUserEmail();
-        } else {
-            return null;
         }
     }
 
     /**
      * Check that the given verification code is correct.
diff --git a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/test/java/org/xwiki/security/authentication/script/AuthenticationScriptServiceTest.java b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/test/java/org/xwiki/security/authentication/script/AuthenticationScriptServiceTest.java
index ccddac5f17b6..03d68a4c1cd4 100644
--- a/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/test/java/org/xwiki/security/authentication/script/AuthenticationScriptServiceTest.java
+++ b/xwiki-platform-core/xwiki-platform-security/xwiki-platform-security-authentication/xwiki-platform-security-authentication-script/src/test/java/org/xwiki/security/authentication/script/AuthenticationScriptServiceTest.java
@@ -193,22 +193,21 @@ void requestResetPassword() throws Exception
         when(this.authorizationManager.hasAccess(Right.PROGRAM)).thenReturn(true);
         UserReference userReference = mock(UserReference.class);
         ResetPasswordRequestResponse requestResponse = mock(ResetPasswordRequestResponse.class);
         when(this.resetPasswordManager.requestResetPassword(userReference)).thenReturn(requestResponse);
         InternetAddress userEmail = new InternetAddress("acme@xwiki.org");
-        when(requestResponse.getUserEmail()).thenReturn(userEmail);
 
-        assertEquals(userEmail, this.scriptService.requestResetPassword(userReference));
+        this.scriptService.requestResetPassword(userReference);
         verify(this.resetPasswordManager).sendResetPasswordEmailRequest(requestResponse);
     }
 
     @Test
     void requestResetPasswordWithoutPR() throws Exception
     {
         when(this.authorizationManager.hasAccess(Right.PROGRAM)).thenReturn(false);
 
-        assertNull(this.scriptService.requestResetPassword(mock(UserReference.class)));
+        this.scriptService.requestResetPassword(mock(UserReference.class));
         verify(this.resetPasswordManager, never()).requestResetPassword(any());
         verify(this.resetPasswordManager, never()).sendResetPasswordEmailRequest(any());
     }
 
     @Test
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/resetpasswordinline.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/resetpasswordinline.vm
index c86a6dd840f2..bf6d2cb36c25 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/resetpasswordinline.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/resetpasswordinline.vm
@@ -75,18 +75,18 @@ $services.localization.render('xe.admin.passwordReset.instructions')
     #if (!$services.csrf.isTokenValid($request.form_token))
         #resetPasswordBoxStart("danger")
         $services.localization.render('xe.admin.passwordReset.error.csrf')
     #else
         #try()
-            #set ($email = $services.security.authentication.requestResetPassword($userName))
+            #set ($discard = $services.security.authentication.requestResetPassword($userName))
         #end
         #if ("$!exception" != '')
             #resetPasswordBoxStart("warning")
             #displayResetPasswordException()
         #else
             #resetPasswordBoxStart("default")
-            $services.localization.render('xe.admin.passwordReset.emailSent', ["$services.mail.general.obfuscate($email)"])
+            $services.localization.render('xe.admin.passwordReset.emailSentToUsername', ["$escapetool.xml($userName)"])
         #end
     #end
   <div>
       <a href="$services.security.authentication.getAuthenticationURL('reset', $NULL)">$services.localization.render('xe.admin.passwordReset.error.retry')</a> |
       <a href="$xwiki.getURL('XWiki.ForgotUsername')">$services.localization.render('xe.admin.passwordReset.error.recoverUsername')</a> |

Vulnerable code:         assertFalse(resetPasswordPage.isResetPasswordSent());
        assertTrue(resetPasswordPage.getMessage().contains("user does not exist"));
        resetPasswordPage = resetPasswordPage.clickRetry();
        assertFalse(resetPasswordPage.isResetPasswordSent());
        assertTrue(resetPasswordPage.getMessage().contains("email address not provided"));
        assertTrue(resetPasswordPage.isResetPasswordSent());
    public boolean isResetPasswordSent()
xe.admin.passwordReset.emailSent=An e-mail was sent to {0}. Please follow the instructions in that e-mail to complete the password reset procedure.
import javax.mail.internet.InternetAddress;

    /**
     * @return the email address of the user for whom the reset password request have been performed.
     */
    InternetAddress getUserEmail();

    private void checkUserReference(UserReference userReference) throws ResetPasswordException
    {
        if (!this.userManager.exists(userReference)) {
            String exceptionMessage =
                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
                    userReference.toString());
            throw new ResetPasswordException(exceptionMessage);
        }
        this.checkUserReference(userReference);

        UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
        InternetAddress email = userProperties.getEmail();

        if (email == null) {
            String exceptionMessage =
                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noEmail");
            throw new ResetPasswordException(exceptionMessage);
        }

        DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
        DocumentReference reference = documentUserReference.getReference();
        XWikiContext context = this.contextProvider.get();

        try {
            XWikiDocument userDocument = context.getWiki().getDocument(reference, context);

            if (userDocument.getXObject(LDAP_CLASS_REFERENCE) != null) {
                String exceptionMessage =
                    this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.ldapUser",
                        userReference.toString());
                throw new ResetPasswordException(exceptionMessage);

            BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE, true, context);
            String verificationCode = context.getWiki().generateRandomString(30);
            xObject.set(VERIFICATION_PROPERTY, verificationCode, context);

            String saveComment =
                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.versionComment");
            context.getWiki().saveDocument(userDocument, saveComment, true, context);

            return new DefaultResetPasswordRequestResponse(userReference, email, verificationCode);
        } catch (XWikiException e) {
            throw new ResetPasswordException("Error when reading user document to perform reset password request.", e);
        AuthenticationResourceReference resourceReference =
            new AuthenticationResourceReference(AuthenticationAction.RESET_PASSWORD);

        UserReference userReference = requestResponse.getUserReference();
        UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
        String serializedUserReference = this.referenceSerializer.serialize(userReference);

        // FIXME: this should be provided as part of the User API.
        String formattedName = "";
        if (!StringUtils.isBlank(userProperties.getFirstName())) {
            formattedName += userProperties.getFirstName();
        }
        if (!StringUtils.isBlank(userProperties.getLastName())) {
            if (!StringUtils.isBlank(formattedName)) {
                formattedName += " ";
            formattedName += userProperties.getLastName();
        }
        if (StringUtils.isBlank(formattedName)) {
            formattedName = serializedUserReference;
        }
        resourceReference.addParameter("u", serializedUserReference);
        resourceReference.addParameter("v", requestResponse.getVerificationCode());

        XWikiContext context = contextProvider.get();

        ExtendedURL extendedURL = null;
        try {
            extendedURL = this.resourceReferenceSerializer.serialize(resourceReference);
            extendedURL = this.urlNormalizer.normalize(extendedURL);
            URL serverURL = context.getURLFactory().getServerURL(context);
            URL externalVerificationURL = new URL(serverURL, extendedURL.serialize());

            this.resetPasswordMailSenderProvider.get()
                .sendResetPasswordEmail(formattedName, requestResponse.getUserEmail(), externalVerificationURL);
        } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException | MalformedURLException e)
        {
            throw new ResetPasswordException("Error when processing information for creating the email.", e);
        this.checkUserReference(userReference);
        XWikiContext context = this.contextProvider.get();

        UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
        InternetAddress email = userProperties.getEmail();

        DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
        DocumentReference reference = documentUserReference.getReference();
        String exceptionMessage =
            this.localizationManager.getTranslationPlain("xe.admin.passwordReset.step2.error.wrongParameters",
                userReference.toString());

        try {
            XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
            BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
            if (xObject == null) {
                throw new ResetPasswordException(exceptionMessage);
            }
            String storedVerificationCode = xObject.getStringValue(VERIFICATION_PROPERTY);
            BaseClass xClass = xObject.getXClass(context);
            PropertyInterface verification = xClass.get(VERIFICATION_PROPERTY);
            if (!(verification instanceof PasswordClass)) {
                throw new ResetPasswordException("Bad definition of ResetPassword XClass.");
            }
            PasswordClass passwordClass = (PasswordClass) verification;
            String equivalentPassword =
                passwordClass.getEquivalentPassword(storedVerificationCode, verificationCode);

            // We ensure to reset the verification code before checking if it's correct to avoid any bruteforce attack.
            String newVerificationCode = context.getWiki().generateRandomString(30);
            xObject.set(VERIFICATION_PROPERTY, newVerificationCode, context);
            String saveComment = this.localizationManager
                .getTranslationPlain("xe.admin.passwordReset.step2.versionComment.changeValidationKey");
            context.getWiki().saveDocument(userDocument, saveComment, true, context);

            if (!storedVerificationCode.equals(equivalentPassword)) {
                throw new ResetPasswordException(exceptionMessage);
            }
            return new DefaultResetPasswordRequestResponse(userReference, email, newVerificationCode);
        } catch (XWikiException e) {
            throw new ResetPasswordException("Cannot open user document to check verification code.", e);
        this.checkUserReference(userReference);
        XWikiContext context = this.contextProvider.get();

        DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
        DocumentReference reference = documentUserReference.getReference();

        try {
            XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
            userDocument.removeXObjects(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
            BaseObject userXObject = userDocument.getXObject(USER_CLASS_REFERENCE);
            userXObject.setStringValue("password", newPassword);

            String saveComment = this.localizationManager.getTranslationPlain(
                "xe.admin.passwordReset.step2.versionComment.passwordReset");
            context.getWiki().saveDocument(userDocument, saveComment, true, context);
        } catch (XWikiException e) {
            throw new ResetPasswordException("Cannot open user document to perform reset password.", e);
import javax.mail.internet.InternetAddress;

    private final InternetAddress userEmail;
     * @param userEmail the email of the user.
    DefaultResetPasswordRequestResponse(UserReference reference, InternetAddress userEmail, String verificationCode)
        this.userEmail = userEmail;
    /**
     * @return the email of the user.
     */
    public InternetAddress getUserEmail()
    {
        return userEmail;
    }

            .append(userEmail, that.userEmail)
            .append(userEmail)
            new DefaultResetPasswordRequestResponse(this.userReference, email,
                verificationCode);
    void requestResetPasswordUnexistingUser()
        String exceptionMessage = "User [user:Foobar] doesn't exist";
        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
            "user:Foobar")).thenReturn(exceptionMessage);
        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
            () -> this.resetPasswordManager.requestResetPassword(this.userReference));
        assertEquals(exceptionMessage, resetPasswordException.getMessage());
        String exceptionMessage = "User has no email address.";
        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noEmail"))
            .thenReturn(exceptionMessage);
        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
            () -> this.resetPasswordManager.requestResetPassword(this.userReference));
        assertEquals(exceptionMessage, resetPasswordException.getMessage());
            new DefaultResetPasswordRequestResponse(this.userReference, email,
                verificationCode);
            new DefaultResetPasswordRequestResponse(this.userReference, email,
                newVerificationCode);
    void checkVerificationCodeUnexistingUser()
        String exceptionMessage = "User [user:Foobar] doesn't exist";
        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
            "user:Foobar")).thenReturn(exceptionMessage);
        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
            () -> this.resetPasswordManager.checkVerificationCode(this.userReference, "some code"));
        assertEquals(exceptionMessage, resetPasswordException.getMessage());
        String exceptionMessage = "User [user:Foobar] doesn't exist";
        when(this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.noUser",
            "user:Foobar")).thenReturn(exceptionMessage);
        ResetPasswordException resetPasswordException = assertThrows(ResetPasswordException.class,
            () -> this.resetPasswordManager.resetPassword(this.userReference, "some password"));
        assertEquals(exceptionMessage, resetPasswordException.getMessage());
import javax.mail.internet.InternetAddress;
     * @return the email address used to send the verification code or {@code null} if the user calling this method
     *         doesn't have programming rights.
    public InternetAddress requestResetPassword(UserReference user) throws ResetPasswordException
            return resetPasswordRequestResponse.getUserEmail();
        } else {
            return null;
        when(requestResponse.getUserEmail()).thenReturn(userEmail);
        assertEquals(userEmail, this.scriptService.requestResetPassword(userReference));
        assertNull(this.scriptService.requestResetPassword(mock(UserReference.class)));
            #set ($email = $services.security.authentication.requestResetPassword($userName))
            $services.localization.render('xe.admin.passwordReset.emailSent', ["$services.mail.general.obfuscate($email)"])
Fixed code:             <revapi.differences>
              <criticality>highlight</criticality>
              <differences>
                <item>
                  <ignore>true</ignore>
                  <code>java.method.returnTypeChanged</code>
                  <old>method javax.mail.internet.InternetAddress org.xwiki.security.authentication.script.AuthenticationScriptService::requestResetPassword(org.xwiki.user.UserReference) throws org.xwiki.security.authentication.ResetPasswordException</old>
                  <new>method void org.xwiki.security.authentication.script.AuthenticationScriptService::requestResetPassword(org.xwiki.user.UserReference) throws org.xwiki.security.authentication.ResetPasswordException</new>
                  <justification>Unstable API: this API was not properly designed and shouldn't have returned that in first place.</justification>
                </item>
                <item>
                  <ignore>true</ignore>
                  <code>java.method.removed</code>
                  <old>method javax.mail.internet.InternetAddress org.xwiki.security.authentication.ResetPasswordRequestResponse::getUserEmail()</old>
                  <justification>Unstable API: this API was not properly designed and shouldn't have returned that in first place.</justification>
                </item>
              </differences>
            </revapi.differences>

        // there should not have any indication if the user exists or not.
        assertTrue(resetPasswordPage.isFormSubmitted());
        resetPasswordPage = ResetPasswordPage.gotoPage();

        // there should not have any indication if an email address is provided or not.
        assertTrue(resetPasswordPage.isFormSubmitted());
        assertTrue(resetPasswordPage.isFormSubmitted());
    /**
     * This method only checks if the form was properly submitted and didn't return an error.
     * It does not mean that an email was necessarily sent.
     *
     * @return {@code true} if the form is properly submitted.
     */
    public boolean isFormSubmitted()
xe.admin.passwordReset.emailSentToUsername=An e-mail was sent to the address configured for user "{0}". Please follow the instructions in that e-mail to complete the password reset procedure.
#######################################
## until 12.10.9, 13.6RC1, 13.4.1
#######################################
xe.admin.passwordReset.emailSent=An e-mail was sent to {0}. Please follow the instructions in that e-mail to complete the password reset procedure.

import org.slf4j.Logger;
    @Inject
    private Logger logger;
    private boolean checkUserReference(UserReference userReference) throws ResetPasswordException
    {

        return this.userManager.exists(userReference);
        if (this.checkUserReference(userReference)) {
            UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
            InternetAddress email = userProperties.getEmail();

            if (email != null) {
                DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
                DocumentReference reference = documentUserReference.getReference();
                XWikiContext context = this.contextProvider.get();

                try {
                    XWikiDocument userDocument = context.getWiki().getDocument(reference, context);

                    if (userDocument.getXObject(LDAP_CLASS_REFERENCE) != null) {
                        String exceptionMessage =
                            this.localizationManager.getTranslationPlain("xe.admin.passwordReset.error.ldapUser",
                                userReference.toString());
                        throw new ResetPasswordException(exceptionMessage);
                    }

                    BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE, true, context);
                    String verificationCode = context.getWiki().generateRandomString(30);
                    xObject.set(VERIFICATION_PROPERTY, verificationCode, context);

                    String saveComment =
                        this.localizationManager.getTranslationPlain("xe.admin.passwordReset.versionComment");
                    context.getWiki().saveDocument(userDocument, saveComment, true, context);

                    return new DefaultResetPasswordRequestResponse(userReference, verificationCode);
                } catch (XWikiException e) {
                    throw new ResetPasswordException(
                        "Error when reading user document to perform reset password request.",
                        e);
                }
            } else {
                // In case the mail is not configured, we log a message to the admin.
                this.logger.info("User [{}] asked to reset their password, but did not have any email configured.",
                    userReference);
        return new DefaultResetPasswordRequestResponse(userReference, null);
        if (this.checkUserReference(requestResponse.getUserReference())) {
            AuthenticationResourceReference resourceReference =
                new AuthenticationResourceReference(AuthenticationAction.RESET_PASSWORD);

            UserReference userReference = requestResponse.getUserReference();
            UserProperties userProperties = this.userPropertiesResolver.resolve(userReference);
            InternetAddress email = userProperties.getEmail();
            String serializedUserReference = this.referenceSerializer.serialize(userReference);

            // FIXME: this should be provided as part of the User API.
            String formattedName = "";
            if (!StringUtils.isBlank(userProperties.getFirstName())) {
                formattedName += userProperties.getFirstName();
            }
            if (!StringUtils.isBlank(userProperties.getLastName())) {
                if (!StringUtils.isBlank(formattedName)) {
                    formattedName += " ";
                }
                formattedName += userProperties.getLastName();
            }
            if (StringUtils.isBlank(formattedName)) {
                formattedName = serializedUserReference;
            }
            resourceReference.addParameter("u", serializedUserReference);
            resourceReference.addParameter("v", requestResponse.getVerificationCode());

            XWikiContext context = contextProvider.get();

            ExtendedURL extendedURL = null;
            try {
                extendedURL = this.resourceReferenceSerializer.serialize(resourceReference);
                extendedURL = this.urlNormalizer.normalize(extendedURL);
                URL serverURL = context.getURLFactory().getServerURL(context);
                URL externalVerificationURL = new URL(serverURL, extendedURL.serialize());

                this.resetPasswordMailSenderProvider.get()
                    .sendResetPasswordEmail(formattedName, email, externalVerificationURL);
            } catch (SerializeResourceReferenceException | UnsupportedResourceReferenceException
                | MalformedURLException e) {
                throw new ResetPasswordException("Error when processing information for creating the email.", e);
        if (this.checkUserReference(userReference)) {
            XWikiContext context = this.contextProvider.get();
            DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
            DocumentReference reference = documentUserReference.getReference();
            String exceptionMessage =
                this.localizationManager.getTranslationPlain("xe.admin.passwordReset.step2.error.wrongParameters",
                    userReference.toString());
            try {
                XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
                BaseObject xObject = userDocument.getXObject(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
                if (xObject == null) {
                    throw new ResetPasswordException(exceptionMessage);
                }

                String storedVerificationCode = xObject.getStringValue(VERIFICATION_PROPERTY);
                BaseClass xClass = xObject.getXClass(context);
                PropertyInterface verification = xClass.get(VERIFICATION_PROPERTY);
                if (!(verification instanceof PasswordClass)) {
                    throw new ResetPasswordException("Bad definition of ResetPassword XClass.");
                }
                PasswordClass passwordClass = (PasswordClass) verification;
                String equivalentPassword =
                    passwordClass.getEquivalentPassword(storedVerificationCode, verificationCode);

                // We ensure to reset the verification code before checking if it's correct to avoid
                // any bruteforce attack.
                String newVerificationCode = context.getWiki().generateRandomString(30);
                xObject.set(VERIFICATION_PROPERTY, newVerificationCode, context);
                String saveComment = this.localizationManager
                    .getTranslationPlain("xe.admin.passwordReset.step2.versionComment.changeValidationKey");
                context.getWiki().saveDocument(userDocument, saveComment, true, context);

                if (!storedVerificationCode.equals(equivalentPassword)) {
                    throw new ResetPasswordException(exceptionMessage);
                }

                return new DefaultResetPasswordRequestResponse(userReference, newVerificationCode);
            } catch (XWikiException e) {
                throw new ResetPasswordException("Cannot open user document to check verification code.", e);
            }
        } else {
            return new DefaultResetPasswordRequestResponse(userReference, null);
        if (this.checkUserReference(userReference)) {
            XWikiContext context = this.contextProvider.get();

            DocumentUserReference documentUserReference = (DocumentUserReference) userReference;
            DocumentReference reference = documentUserReference.getReference();

            try {
                XWikiDocument userDocument = context.getWiki().getDocument(reference, context);
                userDocument.removeXObjects(RESET_PASSWORD_REQUEST_CLASS_REFERENCE);
                BaseObject userXObject = userDocument.getXObject(USER_CLASS_REFERENCE);
                userXObject.setStringValue("password", newPassword);

                String saveComment = this.localizationManager.getTranslationPlain(
                    "xe.admin.passwordReset.step2.versionComment.passwordReset");
                context.getWiki().saveDocument(userDocument, saveComment, true, context);
            } catch (XWikiException e) {
                throw new ResetPasswordException("Cannot open user document to perform reset password.", e);
            }
    DefaultResetPasswordRequestResponse(UserReference reference, String verificationCode)
import org.junit.jupiter.api.extension.RegisterExtension;
import org.xwiki.test.LogLevel;
import org.xwiki.test.junit5.LogCaptureExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
    @RegisterExtension
    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.INFO);

            new DefaultResetPasswordRequestResponse(this.userReference, verificationCode);
    void requestResetPasswordUnexistingUser() throws ResetPasswordException
        assertEquals(new DefaultResetPasswordRequestResponse(this.userReference, null),
            this.resetPasswordManager.requestResetPassword(this.userReference));
        assertEquals(new DefaultResetPasswordRequestResponse(this.userReference, null),
            this.resetPasswordManager.requestResetPassword(this.userReference));
        when(this.userReference.toString()).thenReturn("foo");
        assertEquals("User [foo] asked to reset their password, but did not have any email configured.",
            logCapture.getMessage(0));
        when(this.userManager.exists(this.userReference)).thenReturn(true);
        when(this.userProperties.getEmail()).thenReturn(email);

            new DefaultResetPasswordRequestResponse(this.userReference, verificationCode);
            new DefaultResetPasswordRequestResponse(this.userReference, newVerificationCode);
    void checkVerificationCodeUnexistingUser() throws ResetPasswordException
        ResetPasswordRequestResponse resetPasswordRequestResponse =
            this.resetPasswordManager.checkVerificationCode(this.userReference, "some code");
        assertEquals(new DefaultResetPasswordRequestResponse(this.userReference, null), resetPasswordRequestResponse);
        this.resetPasswordManager.resetPassword(this.userReference, "some password");
        verify(this.xWiki, never()).getDocument(any(DocumentReference.class), any(XWikiContext.class));
        verify(this.xWiki, never()).saveDocument(any(XWikiDocument.class), anyString(), anyBoolean(),
            any(XWikiContext.class));
    public void requestResetPassword(UserReference user) throws ResetPasswordException
        this.scriptService.requestResetPassword(userReference);
        this.scriptService.requestResetPassword(mock(UserReference.class));
            #set ($discard = $services.security.authentication.requestResetPassword($userName))
            $services.localization.render('xe.admin.passwordReset.emailSentToUsername', ["$escapetool.xml($userName)"])