CVE-2021-32730: Cross-Site Request Forgery (CSRF) (CWE-352)
value: 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.
CWE 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.
CVSS v3 base score: 5.7
Published: 2021-07-01T18:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Other
Files changed: xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties, xwiki-platform-core/xwiki-platform-web/src/main/webapp/templates/register_macros.vm
Commit date: 2021-03-03 15:51:46 +0100
Commit message: XWIKI-18315: Bad check in reset password form.

Diff: 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 bb79124e637..b53165e1555 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
@@ -1627,10 +1627,11 @@ core.register.lastName=Last Name
 core.register.username=Username
 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:
 
 # Misc about users
diff --git a/xwiki-platform-core/xwiki-platform-web/src/main/webapp/templates/register_macros.vm b/xwiki-platform-core/xwiki-platform-web/src/main/webapp/templates/register_macros.vm
index 0667667f902..6e8ada698a2 100644
--- a/xwiki-platform-core/xwiki-platform-web/src/main/webapp/templates/register_macros.vm
+++ b/xwiki-platform-core/xwiki-platform-web/src/main/webapp/templates/register_macros.vm
@@ -170,10 +170,11 @@ $xwiki.get('ssfx').use('uicomponents/widgets/validation/livevalidation.css', tru
     #else
       $services.localization.render('xe.admin.registration.fieldWithNoName')
     #end
   #end
   </dl>
+  <input type="hidden" name="form_token" value="$services.csrf.getToken()" ∕>
   #generateJavascript($fields)
 #end
 ##
 #macro (validateRegexJS $regex $fieldName)
   #set ($pattern = "")
@@ -309,78 +310,83 @@ $xwiki.get('ssfx').use('uicomponents/widgets/validation/livevalidation.css', tru
  * @param $request An XWikiRequest object which made the register request, used to get parameters.
  *###
 #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
 
 #*
  * Get the configuration from the configuration object.
  *

Vulnerable code:   #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'))
          #else
          ERROR: In field: ${fieldName}: mustMatch validation required both name
          (of field which this field must match) and failureMessage.
        #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)
        #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'))
          #else
          ERROR: In field: ${fieldName}: programmaticValidation requires code and failureMessage
        #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
      #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
Fixed code: core.register.badCSRF=Bad CSRF token.
  <input type="hidden" name="form_token" value="$services.csrf.getToken()" ∕>
  #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.
        ##
        ## 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'))
        ## 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)
        ##
        ## 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
        #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