CVE-2022-36095 (published 2022-09-08T21:15Z)

XWiki Platform is a generic wiki platform. Prior to versions 13.10.5 and 14.3, it is possible to perform a Cross-Site Request Forgery (CSRF) attack for adding or removing tags on XWiki pages. The problem has been patched in XWiki 13.10.5 and 14.3. As a workaround, one may locally modify the `documentTags.vm` template in one's filesystem, to apply the changes exposed there.

CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/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: 2022-04-21 15:39:27+02:00
msg: XWIKI-19550: Wrong error message in case of missing tag plugin

  * Refactor a bit the template for tag handling
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: documentTags.vm
change_type: ModificationType.MODIFY
diff: @@ -25,72 +25,91 @@
 ##
 ##
 #macro(displayTag $tag)
-<span class="tag-wrapper"><span class="tag"><a href="$xwiki.getURL('Main.Tags', 'view', "do=viewTag&amp;tag=$!{escapetool.url($tag)}")">$!{escapetool.xml($tag)}</a></span>#if($hasedit)<span class="separator">[</span><a href="$doc.getURL('view', "xpage=documentTags&amp;xaction=delete&amp;tag=$!{escapetool.url($tag)}&amp;xredirect=${xredirect}")" class="tag-tool tag-delete" title="$services.localization.render('core.tags.remove.tooltip')">X</a><span class="separator">]</span>#end</span>
+  #set ($viewTagUrl = $xwiki.getURL('Main.Tags', 'view', "do=viewTag&amp;tag=$!{escapetool.url($tag)}"))
+  ## Note that the form_token parameter needs to be kept before the xredirect parameter since the JS code might replace the latter
+  ## All that would need a cleaner fix in the javascript of tags.
+  #set ($deleteTagUrl = $doc.getURL('view', "xpage=documentTags&amp;xaction=delete&amp;tag=$!{escapetool.url($tag)}&amp;form_token=$!{escapetool.url($services.csrf.token)}&amp;xredirect=${xredirect}"))
+<span class="tag-wrapper">
+  <span class="tag"><a href="$viewTagUrl">$!{escapetool.xml($tag)}</a></span>
+  #if($hasedit)<span class="separator">[</span><a href="$deleteTagUrl" class="tag-tool tag-delete" title="$services.localization.render('core.tags.remove.tooltip')">X</a><span class="separator">]</span>#end
+</span>
 #end
 ##
 #macro(removeTag $tag)
-    #if($xwiki.tag)
-        #set($result = $xwiki.tag.removeTagFromDocument($tag, $doc.fullName))
-        #if($result == 'OK' && "$!{request.ajax}" != '')
-            $response.setStatus(200)
-            #set($responseMessage = 'OK')
-        #elseif($result == 'NO_EFFECT')
-            $response.setStatus(409)
-            #set($responseMessage = $services.localization.render('core.tags.remove.error.notFound', [$tag]))
-        #elseif($result == 'NOT_ALLOWED')
-            $response.setStatus(403)
-            #set($responseMessage = $services.localization.render('core.tags.remove.error.notAllowed', [$tag]))
-        #elseif($result == 'FAILED')
-            $response.setStatus(500)
-            #set($responseMessage = $services.localization.render('core.tags.remove.error.failed', [$tag]))
-        #end
-        #if("$!{request.ajax}" != '')
-            $!responseMessage
-        #elseif("$!{request.xredirect}" != '')
-            $response.sendRedirect($request.xredirect)
+    #if ($services.csrf.isTokenValid($request.get('form_token')))
+        #if($xwiki.tag)
+            #set($result = $xwiki.tag.removeTagFromDocument($tag, $doc.fullName))
+            #if($result == 'OK' && "$!{request.ajax}" != '')
+                #set ($discard= $response.setStatus(200))
+                #set($responseMessage = 'OK')
+            #elseif($result == 'NO_EFFECT')
+                #set ($discard= $response.setStatus(409))
+                #set($responseMessage = $services.localization.render('core.tags.remove.error.notFound', [$tag]))
+            #elseif($result == 'NOT_ALLOWED')
+                #set ($discard= $response.setStatus(403))
+                #set($responseMessage = $services.localization.render('core.tags.remove.error.notAllowed', [$tag]))
+            #elseif($result == 'FAILED')
+                #set ($discard= $response.setStatus(500))
+                #set($responseMessage = $services.localization.render('core.tags.remove.error.failed', [$tag]))
+            #end
+        #else
+            #set ($discard= $response.setStatus(501))
+            #set ($responseMessage = "Tag plugin is missing")
         #end
     #else
-        ## TODO
+        #set ($discard= $response.setStatus(401))
+        #set($responseMessage = $services.localization.render('core.tags.remove.error.notAllowed', [$tag]))
+    #end
+    #if("$!{request.ajax}" != '')
+        $!responseMessage
+    #elseif("$!{request.xredirect}" != '')
+        $response.sendRedirect($request.xredirect)
     #end
 #end
 ##
 #macro(addTag $tag)
-    #if($xwiki.tag)
-        #set($oldTags = $xwiki.tag.getTagsFromDocument($doc.fullName))
-        #set($result = $xwiki.tag.addTagsToDocument($tag, $doc.fullName))
-        #if($result == 'OK' && "$!{request.ajax}" != '')
-            #set($newTags = $xwiki.tag.getTagsFromDocument($doc.fullName))
-            #set($discard = $newTags.removeAll($oldTags))
-            #foreach($t in $newTags)
-                #if($t != '' && !$oldTags.contains($t))
-                    #displayTag($t)
+    #if ($services.csrf.isTokenValid($request.get('form_token')))
+        #if($xwiki.tag)
+            #set($oldTags = $xwiki.tag.getTagsFromDocument($doc.fullName))
+            #set($result = $xwiki.tag.addTagsToDocument($tag, $doc.fullName))
+            #if($result == 'OK' && "$!{request.ajax}" != '')
+                #set($newTags = $xwiki.tag.getTagsFromDocument($doc.fullName))
+                #set($discard = $newTags.removeAll($oldTags))
+                #foreach($t in $newTags)
+                    #if($t != '' && !$oldTags.contains($t))
+                        #displayTag($t)
+                    #end
                 #end
+            #elseif($result == 'NO_EFFECT')
+                $response.setStatus(409)
+                #set($tagErrorMessage = $services.localization.render('core.tags.add.error.alreadySet', [$tag]))
+            #elseif($result == 'NOT_ALLOWED')
+                $response.setStatus(403)
+                #set($tagErrorMessage = $services.localization.render('core.tags.add.error.notAllowed', [$tag]))
+            #elseif($result == 'FAILED')
+                $response.setStatus(500)
+                #set($tagErrorMessage = $services.localization.render('core.tags.add.error.failed', [$tag]))
             #end
-        #elseif($result == 'NO_EFFECT')
-            $response.setStatus(409)
-            #set($tagErrorMessage = $services.localization.render('core.tags.add.error.alreadySet', [$tag]))
-        #elseif($result == 'NOT_ALLOWED')
-            $response.setStatus(403)
-            #set($tagErrorMessage = $services.localization.render('core.tags.add.error.notAllowed', [$tag]))
-        #elseif($result == 'FAILED')
-            $response.setStatus(500)
-            #set($tagErrorMessage = $services.localization.render('core.tags.add.error.failed', [$tag]))
-        #end
-        #if("$!{request.ajax}" != '')
-            $tagErrorMessage
-        #elseif("$!{request.xredirect}" != '')
-            $response.sendRedirect($request.xredirect)
+            #if("$!{request.ajax}" != '')
+                $tagErrorMessage
+            #elseif("$!{request.xredirect}" != '')
+                $response.sendRedirect($request.xredirect)
+            #end
+        #else
+            #set ($discard= $response.setStatus(501))
+            #set ($responseMessage = "Tag plugin is missing")
         #end
     #else
-        ## TODO
+        #set ($discard= $response.setStatus(401))
+        #set($responseMessage = $services.localization.render('core.tags.add.error.notAllowed', [$tag]))
     #end
 #end
 ##
 #macro(displayAddForm)
-<form action="$doc.getURL('view', "xpage=documentTags&amp;xaction=add&amp;xredirect=${xredirect}")" method="post" class="tag-add-form">
+## Note that the form_token parameter needs to be kept before the xredirect parameter since the JS code might replace the latter
+## All that would need a cleaner fix in the javascript of tags.
+<form action="$doc.getURL('view', "xpage=documentTags&amp;xaction=add&amp;form_token=$!{escapetool.url($services.csrf.token)}&amp;xredirect=${xredirect}")" method="post" class="tag-add-form">
   <div>
-      ## CSRF prevention
-    <div class="hidden"><input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /></div>
     <label for="tag">$services.localization.render('core.tags.add.label')<br/>
       <input class="input-tag" type="text" id="tag" name="tag" autocomplete="off"/></label><br/>
     <span class="buttonwrapper"><input class="button button-add-tag" type="submit" value="$services.localization.render('core.tags.add.submit')"/></span>

programming_language: CoffeeScript