Cross-Site Request Forgery in xwiki-platform
Description
Impact
It's possible to know if a user has or not an account in a wiki related to an email address, and which username(s) is actually tied to that email by forging a request to the Forgot username page. Note that since this page does not have a CSRF check it's quite easy to perform a lot of those requests.
Patches
This issue has been patched in XWiki 12.10.5 and 13.2RC1. Two different patches are provided: - a first one to fix the CSRF problem - a more complex one that now relies on sending an email for the Forgot username process.
Workarounds
It's possible to fix the problem without uprading by editing the ForgotUsername page in version below 13.x, to use the following code: https://github.com/xwiki/xwiki-platform/blob/69548c0320cbd772540cf4668743e69f879812cf/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ForgotUsername.xml#L39-L123 In version after 13.x it's also possible to edit manually the forgotusername.vm file, but it's really encouraged to upgrade the version here.
References * https://jira.xwiki.org/browse/XWIKI-18384 * https://jira.xwiki.org/browse/XWIKI-18408
For more information
If you have any questions or comments about this advisory: * Open an issue in Jira XWiki * Email us at security ML
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-administration-uiMaven | < 12.10.5 | 12.10.5 |
org.xwiki.platform:xwiki-platform-administration-uiMaven | >= 13.0, < 13.2RC1 | 13.2RC1 |
Affected products
1- Range: < 12.10.5
Patches
269548c0320cbXWIKI-18408: Wrong check in forgot username
1 file changed · +9 −2
xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ForgotUsername.xml+9 −2 modified@@ -38,12 +38,19 @@ <hidden>true</hidden> <content>{{velocity}} #set($email = "$!request.get('e')") -#if($email == '') +#if($email == '' || !$services.csrf.isTokenValid($request.form_token)) {{translation key="xe.admin.forgotUsername.instructions"/}} {{html}} <form method="post" action="$doc.getURL()" class="xformInline"> - <div><label for="e">$services.localization.render('xe.admin.forgotUsername.email.label')</label> <input type="text" id="e" name="e"/> <span class="buttonwrapper"><input type="submit" value="$services.localization.render('xe.admin.forgotUsername.submit')" class="button"/></span></div> + <div> + <label for="e">$services.localization.render('xe.admin.forgotUsername.email.label')</label> + <input type="text" id="e" name="e"/> + <input type="hidden" name="form_token" value="$services.csrf.getToken()"/> + <span class="buttonwrapper"> + <input type="submit" value="$services.localization.render('xe.admin.forgotUsername.submit')" class="button"/> + </span> + </div> </form> {{/html}}
f0440dfcbba7XWIKI-18384: Improve ForgotUsername process
5 files changed · +400 −32
xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-docker/src/test/it/org/xwiki/administration/test/ui/ForgotUsernameIT.java+161 −16 modified@@ -19,12 +19,30 @@ */ package org.xwiki.administration.test.ui; +import java.util.HashMap; +import java.util.Map; + +import javax.mail.Address; +import javax.mail.BodyPart; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeMessage; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.xwiki.administration.test.po.ForgotUsernameCompletePage; import org.xwiki.administration.test.po.ForgotUsernamePage; +import org.xwiki.test.docker.junit5.TestConfiguration; import org.xwiki.test.docker.junit5.UITest; +import org.xwiki.test.integration.junit.LogCaptureConfiguration; import org.xwiki.test.ui.TestUtils; +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetupTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,44 +52,171 @@ * @version $Id$ * @since 11.10 */ -@UITest +@UITest(sshPorts = { + // Open the GreenMail port so that the XWiki instance inside a Docker container can use the SMTP server provided + // by GreenMail running on the host. + 3025 +}, + properties = { + // The Mail module contributes a Hibernate mapping that needs to be added to hibernate.cfg.xml + "xwikiDbHbmCommonExtraMappings=mailsender.hbm.xml" + }, + extraJARs = { + // It's currently not possible to install a JAR contributing a Hibernate mapping file as an Extension. Thus + // we need to provide the JAR inside WEB-INF/lib. See https://jira.xwiki.org/browse/XWIKI-8271 + "org.xwiki.platform:xwiki-platform-mail-send-storage" + } +) public class ForgotUsernameIT { + private GreenMail mail; + + @BeforeEach + public void startMail(TestUtils setup, TestConfiguration testConfiguration) + { + this.mail = new GreenMail(ServerSetupTest.SMTP); + this.mail.start(); + + configureEmail(setup, testConfiguration); + } + + @AfterEach + public void stopMail(TestUtils setup, LogCaptureConfiguration logCaptureConfiguration) + { + if (this.mail != null) { + this.mail.stop(); + } + + restoreSettings(setup); + logCaptureConfiguration.registerExcludes("CSRFToken: Secret token verification failed, token"); + } + + private void configureEmail(TestUtils setup, TestConfiguration testConfiguration) + { + setup.updateObject("Mail", "MailConfig", "Mail.SendMailConfigClass", 0, "host", + testConfiguration.getServletEngine().getHostIP(), "port", "3025", "sendWaitTime", "0"); + } + + private void restoreSettings(TestUtils setup) + { + // Make sure we can restore the settings, so we log back with superadmin to finish the work + setup.loginAsSuperAdmin(); + + // Remove the previous version that the setup has created. + setup.deleteLatestVersion("Mail", "MailConfig"); + } + + private Map<String, String> getMessageContent(MimeMessage message) throws Exception + { + Map<String, String> messageMap = new HashMap<>(); + + Address[] addresses = message.getAllRecipients(); + assertTrue(addresses.length == 1); + messageMap.put("recipient", addresses[0].toString()); + + messageMap.put("subjectLine", message.getSubject()); + + Multipart mp = (Multipart) message.getContent(); + + BodyPart plain = getPart(mp, "text/plain"); + if (plain != null) { + messageMap.put("textPart", IOUtils.toString(plain.getInputStream(), "UTF-8")); + } + BodyPart html = getPart(mp, "text/html"); + if (html != null) { + messageMap.put("htmlPart", IOUtils.toString(html.getInputStream(), "UTF-8")); + } + + return messageMap; + } + + private BodyPart getPart(Multipart messageContent, String mimeType) throws Exception + { + for (int i = 0; i < messageContent.getCount(); i++) { + BodyPart part = messageContent.getBodyPart(i); + + if (part.isMimeType(mimeType)) { + return part; + } + + if (part.isMimeType("multipart/related") || part.isMimeType("multipart/alternative") + || part.isMimeType("multipart/mixed")) + { + BodyPart out = getPart((Multipart) part.getContent(), mimeType); + if (out != null) { + return out; + } + } + } + return null; + } + @Test - public void retrieveUsername(TestUtils testUtils) + public void retrieveUsername(TestUtils testUtils) throws Exception { - String user = "realuser"; - String userMail = "realuser@host.org"; + // We create three users, two of them are sharing the same email + String user1Login = "realuser1"; + String user1Email = "realuser@host.org"; + + String user2Login = "realuser2"; + String user2Email = "realuser@host.org"; + + String user3Login = "foo"; + String user3Email = "foo@host.org"; // We need to login as superadmin to set the user email. testUtils.loginAsSuperAdmin(); - testUtils.createUser(user, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", userMail); + testUtils.createUser(user1Login, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", user1Email); + testUtils.createUser(user2Login, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", user2Email); + testUtils.createUser(user3Login, "realuserpwd", testUtils.getURLToNonExistentPage(), "email", user3Email); testUtils.forceGuestUser(); + + // check that when asking to retrieve username with a wrong email we don't get any information + // if an user exists or not and no email is sent. ForgotUsernamePage forgotUsernamePage = ForgotUsernamePage.gotoPage(); - forgotUsernamePage.setEmail(userMail); + forgotUsernamePage.setEmail("notexistant@xwiki.com"); ForgotUsernameCompletePage forgotUsernameCompletePage = forgotUsernamePage.clickRetrieveUsername(); - assertFalse(forgotUsernameCompletePage.isAccountNotFound()); - assertTrue(forgotUsernameCompletePage.isUsernameRetrieved(user)); + assertTrue(forgotUsernameCompletePage.isForgotUsernameQuerySent()); + + // we are waiting 5 sec here just to be sure no mail is sent, maybe we could decrease the timeout value, + // not sure. + assertFalse(this.mail.waitForIncomingEmail(1)); // Bypass the check that prevents to reload the current page testUtils.gotoPage(testUtils.getURLToNonExistentPage()); - // test that bad mail results in no results + // test getting email for a forgot username request where the email is set in one account only forgotUsernamePage = ForgotUsernamePage.gotoPage(); - forgotUsernamePage.setEmail("bad_mail@evil.com"); + forgotUsernamePage.setEmail(user3Email); forgotUsernameCompletePage = forgotUsernamePage.clickRetrieveUsername(); - assertTrue(forgotUsernameCompletePage.isAccountNotFound()); - assertFalse(forgotUsernameCompletePage.isUsernameRetrieved(user)); + assertTrue(forgotUsernameCompletePage.isForgotUsernameQuerySent()); + assertTrue(this.mail.waitForIncomingEmail(1)); + MimeMessage[] receivedEmails = this.mail.getReceivedMessages(); + assertEquals(1, receivedEmails.length); + MimeMessage receivedEmail = receivedEmails[0]; + assertTrue(receivedEmail.getSubject().contains("Forgot username on")); + String receivedMailContent = getMessageContent(receivedEmail).get("textPart"); + assertTrue(receivedMailContent.contains(String.format("XWiki.%s", user3Login))); + + // remove mails for last test + this.mail.purgeEmailFromAllMailboxes(); // Bypass the check that prevents to reload the current page testUtils.gotoPage(testUtils.getURLToNonExistentPage()); - // XWIKI-4920 test that the email is properly escaped + // test getting email for a forgot username request where the email is set in two accounts forgotUsernamePage = ForgotUsernamePage.gotoPage(); - forgotUsernamePage.setEmail("a' synta\\'x error"); + forgotUsernamePage.setEmail(user1Email); forgotUsernameCompletePage = forgotUsernamePage.clickRetrieveUsername(); - assertTrue(forgotUsernameCompletePage.isAccountNotFound()); - assertFalse(forgotUsernameCompletePage.isUsernameRetrieved(user)); + assertTrue(forgotUsernameCompletePage.isForgotUsernameQuerySent()); + assertTrue(this.mail.waitForIncomingEmail(1)); + receivedEmails = this.mail.getReceivedMessages(); + assertEquals(1, receivedEmails.length); + receivedEmail = receivedEmails[0]; + assertTrue(receivedEmail.getSubject().contains("Forgot username on")); + receivedMailContent = getMessageContent(receivedEmail).get("textPart"); + assertTrue(receivedMailContent.contains(String.format("XWiki.%s", user1Login))); + assertTrue(receivedMailContent.contains(String.format("XWiki.%s", user2Login))); } }
xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-test/xwiki-platform-administration-test-pageobjects/src/main/java/org/xwiki/administration/test/po/ForgotUsernameCompletePage.java+34 −0 modified@@ -21,6 +21,7 @@ import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; +import org.xwiki.stability.Unstable; import org.xwiki.test.ui.po.ViewPage; /** @@ -31,6 +32,10 @@ */ public class ForgotUsernameCompletePage extends ViewPage { + /** + * @deprecated since 12.10.5 and 13.2RC1 this message is no longer displayed. + */ + @Deprecated public boolean isUsernameRetrieved(String username) { try { @@ -42,8 +47,37 @@ public boolean isUsernameRetrieved(String username) } } + /** + * @deprecated since 12.10.5 and 13.2RC1 this message is no longer displayed. + */ + @Deprecated public boolean isAccountNotFound() { return getContent().contains("No account is registered using this email address"); } + + /** + * @return the text content of the message box. + * @since 12.10.5 + * @since 13.2RC1 + */ + @Unstable + public String getMessage() + { + return getContent(); + } + + /** + * @return {@code true} if the forgot username query was successfully sent (without any error). + * @since 12.10.5 + * @since 13.2RC1 + */ + @Unstable + public boolean isForgotUsernameQuerySent() + { + // If there is no form and we see an info box, then the request was sent. + return !getDriver().hasElementWithoutWaiting(By.cssSelector("#forgotUsernameForm")) + && getMessage().contains("If an account is registered with this email, " + + "you will receive the account information on"); + } }
xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ForgotUsernameMailContent.xml+141 −0 added@@ -0,0 +1,141 @@ +<?xml version="1.1" encoding="UTF-8"?> + +<!-- + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. +--> + +<xwikidoc version="1.4" reference="XWiki.ForgotUsernameMailContent" locale=""> + <web>XWiki</web> + <name>ForgotUsernameMailContent</name> + <language/> + <defaultLanguage/> + <translation>0</translation> + <creator>xwiki:XWiki.Admin</creator> + <parent>WebHome</parent> + <author>xwiki:XWiki.Admin</author> + <contentAuthor>xwiki:XWiki.Admin</contentAuthor> + <version>1.1</version> + <title>Forgot Username email</title> + <comment/> + <minorEdit>false</minorEdit> + <syntaxId>xwiki/2.0</syntaxId> + <hidden>true</hidden> + <content/> + <object> + <name>XWiki.ForgotUsernameMailContent</name> + <number>0</number> + <className>XWiki.Mail</className> + <guid>5940d653-0e58-439f-ad1a-9e1afbecf9dc</guid> + <class> + <name>XWiki.Mail</name> + <customClass/> + <customMapping/> + <defaultViewSheet/> + <defaultEditSheet/> + <defaultWeb/> + <nameField/> + <validationScript/> + <html> + <contenttype>PureText</contenttype> + <disabled>0</disabled> + <editor>PureText</editor> + <name>html</name> + <number>4</number> + <prettyName>HTML</prettyName> + <rows>15</rows> + <size>80</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType> + </html> + <language> + <disabled>0</disabled> + <name>language</name> + <number>2</number> + <prettyName>Language</prettyName> + <size>5</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.StringClass</classType> + </language> + <subject> + <disabled>0</disabled> + <name>subject</name> + <number>1</number> + <prettyName>Subject</prettyName> + <size>40</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.StringClass</classType> + </subject> + <text> + <contenttype>PureText</contenttype> + <disabled>0</disabled> + <editor>PureText</editor> + <name>text</name> + <number>3</number> + <prettyName>Text</prettyName> + <rows>15</rows> + <size>80</size> + <unmodifiable>0</unmodifiable> + <classType>com.xpn.xwiki.objects.classes.TextAreaClass</classType> + </text> + </class> + <property> + <html><h2>Hello,</h2> +#set ($wikiurl = $xwiki.getURL($services.model.resolveDocument('', 'default', $doc.documentReference.extractReference('WIKI')))) +#set ($xwikiLoginURL = $xwiki.getURL('XWiki.XWikiLogin', 'login')) +#set ($wikiname = $request.serverName) +#set ($severalUsernames = ($usernames.size() > 0)) +<p>A forgot username request has been performed on <a href="$wikiurl">$wikiname</a>. +If you did not make the request, please ignore this message.</p> +<p>We found the following username#if($severalUsernames)s#end related to this email address: +:<br/> +#if ($severalUsernames) +<ul> +#foreach ($username in $usernames) + <li>$username</li> +#end +</ul> +#else +<strong>$usernames.get(0)</strong> +#end + +<p> +You can login from this page: <a href="$xwikiLoginURL">XWiki Login</a>. +</p> +</html> + </property> + <property> + <language>en</language> + </property> + <property> + <subject>Forgot username on ${request.getServerName()}</subject> + </property> + <property> + <text>Hello, + +A forgot username request has been made on ${request.getServerName()}. +We found the following username(s) related to this email address: + +#foreach ($username in $usernames) + $usernames +#end + +You can login with this username from this URL: $xwiki.getURL('XWiki.XWikiLogin', 'login').</text> + </property> + </object> +</xwikidoc>
xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ForgotUsername.xml+54 −12 modified@@ -54,22 +54,64 @@ #if($results.size() == 0 && ${xcontext.database} != ${xcontext.mainWikiName}) #set($results = $query.setWiki("${xcontext.mainWikiName}").execute()) #end - #if($results.size() == 0) - {{translation key="xe.admin.forgotUsername.error.noAccount"/}} + #set ($emailError = false) + #if($results.size() != 0) + ## Send the email + #set ($from = $services.mail.sender.configuration.fromAddress) + #if ("$!from" == '') + #set ($from = "no-reply@${request.serverName}") + #end + ## The mail template use $usernames to display the results. + #set ($usernames = $results) + #set ($mailTemplateReference = $services.model.createDocumentReference('', 'XWiki', 'ForgotUsernameMailContent')) + #set ($mailParameters = {'from' : $from, 'to' : $email, 'language' : $xcontext.locale}) + #set ($message = $services.mail.sender.createMessage('template', $mailTemplateReference, $mailParameters)) + #set ($discard = $message.setType('Forgot Username')) + #macro (displayError $text) - [[{{translation key="xe.admin.forgotUsername.error.retry"/}}>>$doc.fullName]] | [[{{translation key="xe.admin.forgotUsername.login"/}}>>path:${xwiki.getURL('XWiki.XWikiLogin', 'login')}]] - #elseif($results.size() == 1) - $services.localization.render('xe.admin.forgotUsername.result', ["**${results.get(0).substring($results.get(0).indexOf('.')).substring(1)}**"]) + {{html}} + <div class="xwikirenderingerror" title="Click to get more details about the error" style="cursor: pointer;"> + $services.localization.render('xe.admin.forgotUsername.error.emailFailed') + </div> + <div class="xwikirenderingerrordescription hidden"> + <pre>${text}</pre> + </div> + {{/html}} - [[{{translation key="xe.admin.forgotUsername.login"/}}>>path:${xwiki.getURL('XWiki.XWikiLogin', 'login')}]] - #else - {{translation key="xe.admin.forgotUsername.multipleResults"/}} - #foreach($item in $results) - * **${item.substring($item.indexOf('.')).substring(1)}** + #set ($emailError = true) + #end + ## Check for an error constructing the message! + #if ($services.mail.sender.lastError) + #displayError($exceptiontool.getStackTrace($services.mail.sender.lastError)) + #else + ## Send the message and wait for it to be sent or for any error to be raised. + #set ($mailResult = $services.mail.sender.send([$message], 'database')) + ## Check for errors during the send + #if ($services.mail.sender.lastError) + #displayError($exceptiontool.getStackTrace($services.mail.sender.lastError)) + #else + #set ($failedMailStatuses = $mailResult.statusResult.getAllErrors()) + #if ($failedMailStatuses.hasNext()) + #set ($mailStatus = $failedMailStatuses.next()) + #displayError($mailStatus.errorDescription) + #end + #end + #end #end + ## We always display a success message even if there's no user found to avoid disclosing information + ## about the users registered on the wiki. + #if (!$emailError) + {{success}} + $services.localization.render('xe.admin.forgotUsername.emailSent', ["$email"]) - [[{{translation key="xe.admin.forgotUsername.login"/}}>>path:${xwiki.getURL('XWiki.XWikiLogin', 'login')}]] - #end + {{html}} + <div> + <a href="$xwiki.getURL('XWiki.XWikiLogin', 'login')">$services.localization.render('xe.admin.forgotUsername.login')</a> + </div> + {{/html}} + + {{/success}} + #end #end {{/velocity}}</content> <object>
xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties+10 −4 modified@@ -2436,16 +2436,14 @@ xe.admin.skin.testskin=Test this skin ### Username recovery xe.admin.forgotUsername.loginMessage=Forgot your username? - xe.admin.forgotUsername.title=Forgot your username? xe.admin.forgotUsername.instructions=Please enter the email address you provided when creating your account. xe.admin.forgotUsername.email.label=Email address xe.admin.forgotUsername.submit=Retrieve username -xe.admin.forgotUsername.result=Your username is: {0} -xe.admin.forgotUsername.multipleResults=The following usernames are registered with this email address: xe.admin.forgotUsername.login=Login \u00BB -xe.admin.forgotUsername.error.noAccount=No account is registered using this email address. xe.admin.forgotUsername.error.retry=\u00AB Try again using another email address +xe.admin.forgotUsername.error.emailFailed=An unknown problem occurred while sending the forgot username email. +xe.admin.forgotUsername.emailSent=If an account is registered with this email, you will receive the account information on {0}. ### Password reset xe.admin.passwordReset.loginMessage=Forgot your password? @@ -5483,6 +5481,14 @@ core.editors.object.delete.confirm=Are you sure you want to delete this object? core.viewers.jump.dialog.invalidNameError=Invalid page name. Valid names have the following format: Space.Page core.viewers.jump.suggest.noResults=No pages found +####################################### +## until 12.10.5, 13.2RC1 +####################################### + +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. + ## Used to indicate where deprecated keys end #@deprecatedend
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-vh5c-jqfg-mhrhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32732ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/69548c0320cbd772540cf4668743e69f879812cfghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/commit/f0440dfcbba705e03f7565cd88893dde57ca3fa8ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-vh5c-jqfg-mhrhghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-18384ghsax_refsource_MISCWEB
- jira.xwiki.org/browse/XWIKI-18408ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.