CVE-2021-32730 (published 2021-07-01T18:15Z)

XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. A cross-site request forgery vulnerability exists in versions prior to 12.10.5, and in versions 13.0 through 13.1. It's possible for forge an URL that, when accessed by an admin, will reset the password of any user in XWiki. The problem has been patched in XWiki 12.10.5 and 13.2RC1. As a workaround, it is possible to apply the patch manually by modifying the `register_macros.vm` template.

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 5.7, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-352
cwe_name: Cross-Site Request Forgery (CSRF)
description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
extended_description: When a web server is designed to receive a request from a client without any mechanism for verifying that it was intentionally sent, then it might be possible for an attacker to trick a client into making an unintentional request to the web server which will be treated as an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Simon Urli
author_date: 2021-03-03 15:51:46+01:00
msg: XWIKI-18315: Bad check in reset password form.
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: ApplicationResources.properties
change_type: ModificationType.MODIFY
diff: @@ -1629,6 +1629,7 @@ core.register.password=Password
 core.register.passwordRepeat=Confirm Password
 core.register.email=Email Address
 core.register.submit=Register
+core.register.badCSRF=Bad CSRF token.
 
 # User account validation
 core.users.activation.validationKey.label=Validation key:

programming_language: Shell
filename: register_macros.vm
change_type: ModificationType.MODIFY
diff: @@ -172,6 +172,7 @@ $xwiki.get('ssfx').use('uicomponents/widgets/validation/livevalidation.css', tru
     #end
   #end
   </dl>
+  <input type="hidden" name="form_token" value="$services.csrf.getToken()" ∕>
   #generateJavascript($fields)
 #end
 ##
@@ -311,74 +312,79 @@ $xwiki.get('ssfx').use('uicomponents/widgets/validation/livevalidation.css', tru
 #macro(validateFields, $fields, $request)
   #set ($allFieldsValid = true)
   #set ($allFieldsErrors = [])
-  #foreach($field in $fields)
-    #if($field.get('validate') && $field.get('name'))
-      #set($fieldName = $field.get('name'))
-      #set($validate = $field.get('validate'))
-      #set($error = '')
-      #set($value = $request.get($fieldName))
-      #if("$!value" != '' || $field.get('type') == 'html')
-      ##
-      ## mustMatch validation
-        #if($error == '' && $validate.get('mustMatch'))
-          #set($mustMatch = $validate.get('mustMatch'))
-          #if($mustMatch.get('name') && $mustMatch.get('failureMessage'))
-            #if($request.get($fieldName) != $request.get($mustMatch.get('name')))
-              #set($error = $mustMatch.get('failureMessage'))
+  #if (!$services.csrf.isTokenValid($request.form_token))
+    #set ($allFieldsValid = false)
+    #set ($discard = $allFieldsErrors.add($services.localization.render('core.register.badCSRF')))
+  #else
+    #foreach($field in $fields)
+      #if($field.get('validate') && $field.get('name'))
+        #set($fieldName = $field.get('name'))
+        #set($validate = $field.get('validate'))
+        #set($error = '')
+        #set($value = $request.get($fieldName))
+        #if("$!value" != '' || $field.get('type') == 'html')
+        ##
+        ## mustMatch validation
+          #if($error == '' && $validate.get('mustMatch'))
+            #set($mustMatch = $validate.get('mustMatch'))
+            #if($mustMatch.get('name') && $mustMatch.get('failureMessage'))
+              #if($request.get($fieldName) != $request.get($mustMatch.get('name')))
+                #set($error = $mustMatch.get('failureMessage'))
+              #end
+            #else
+            ERROR: In field: ${fieldName}: mustMatch validation required both name
+            (of field which this field must match) and failureMessage.
             #end
-          #else
-          ERROR: In field: ${fieldName}: mustMatch validation required both name
-          (of field which this field must match) and failureMessage.
           #end
-        #end
-      ##
-      ## Regex validation
-      ## We won't bother with regex validation if there is no entry, that would defeat the purpose of 'mandatory'
-        #if($error == '' && $validate.get('regex') && $value && $value != '')
-          #set($regex = $validate.get('regex'))
-          #validateRegex($value, $fieldName, $regex, $error)
-        #end
-      ## List of regex validation
-        #if($error == '' && $validate.get('regexes') && $value && $value != '')
-          #set($regexes = $validate.get('regexes'))
-          #foreach ($regex in $regexes)
+        ##
+        ## Regex validation
+        ## We won't bother with regex validation if there is no entry, that would defeat the purpose of 'mandatory'
+          #if($error == '' && $validate.get('regex') && $value && $value != '')
+            #set($regex = $validate.get('regex'))
             #validateRegex($value, $fieldName, $regex, $error)
           #end
-        #end
-      ##
-      ## If regex and mustMatch validation passed, try programmatic validation
-        #if($error == '' && $validate.get('programmaticValidation'))
-          #set($pv = $validate.get('programmaticValidation'))
-          #if($pv.get('code') && $pv.get('failureMessage'))
-            #set($pvReturn = "#evaluate($pv.get('code'))")
-            #if($pvReturn.indexOf('failed') != -1)
-              #set($error = $pv.get('failureMessage'))
+        ## List of regex validation
+          #if($error == '' && $validate.get('regexes') && $value && $value != '')
+            #set($regexes = $validate.get('regexes'))
+            #foreach ($regex in $regexes)
+              #validateRegex($value, $fieldName, $regex, $error)
             #end
-          #else
-          ERROR: In field: ${fieldName}: programmaticValidation requires code and failureMessage
           #end
-        #end
-      #else
-      ##
-      ## If no content, check if content is mandatory
-        #if($validate.get('mandatory'))
-          #set($mandatory = $validate.get('mandatory'))
-          #if($mandatory.get('failureMessage'))
-            #set($error = $mandatory.get('failureMessage'))
-          #else
-          ERROR: In field: ${fieldName}: mandatory validation requires a failureMessage
+        ##
+        ## If regex and mustMatch validation passed, try programmatic validation
+          #if($error == '' && $validate.get('programmaticValidation'))
+            #set($pv = $validate.get('programmaticValidation'))
+            #if($pv.get('code') && $pv.get('failureMessage'))
+              #set($pvReturn = "#evaluate($pv.get('code'))")
+              #if($pvReturn.indexOf('failed') != -1)
+                #set($error = $pv.get('failureMessage'))
+              #end
+            #else
+            ERROR: In field: ${fieldName}: programmaticValidation requires code and failureMessage
+            #end
+          #end
+        #else
+        ##
+        ## If no content, check if content is mandatory
+          #if($validate.get('mandatory'))
+            #set($mandatory = $validate.get('mandatory'))
+            #if($mandatory.get('failureMessage'))
+              #set($error = $mandatory.get('failureMessage'))
+            #else
+            ERROR: In field: ${fieldName}: mandatory validation requires a failureMessage
+            #end
           #end
         #end
-      #end
-      #if($error != '')
-        #set($discard = $field.put('error', $error))
-        #set ($discard = $allFieldsErrors.add($error))
-        #set($allFieldsValid = false)
-      #end
-    #elseif(!$field.get('name'))
-    ERROR: Field with no name.
-    #end##if(validate)
-  #end##loop
+        #if($error != '')
+          #set($discard = $field.put('error', $error))
+          #set ($discard = $allFieldsErrors.add($error))
+          #set($allFieldsValid = false)
+        #end
+      #elseif(!$field.get('name'))
+      ERROR: Field with no name.
+      #end##if(validate)
+    #end##loop
+  #end ## CSRF check
 #end##macro
 
 #*

programming_language: Ruby