CVE-2022-23619 (Java, published 2022-02-09T21:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-640 \u2014 Weak Password Recovery Mechanism for Forgotten Password
The product contains a mechanism for users to recover or change their passwords without knowing the original password, but the mechanism is weak.
Repository: https://github.com/xwiki/xwiki-platform
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 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

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)"])