CVE-2022-36095: Cross-Site Request Forgery (CSRF) (CWE-352)
value: 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.
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: 4.3
Published: 2022-09-08T21:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Other
Files changed: xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/documentTags.vm
Commit date: 2022-04-21 15:53:56 +0200
Commit message: XWIKI-19550: Wrong error message in case of missing tag plugin

  * Refactor a bit the template for tag handling

Diff: diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/documentTags.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/documentTags.vm
index 8a3b39c561c0..f5198e66fd65 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/documentTags.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/documentTags.vm
@@ -23,76 +23,95 @@
 #set($xredirect = $escapetool.url($doc.getURL('view', "#${tagsId}")))
 ##
 ##
 ##
 #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>
     <span class="buttonwrapper"><a class="button button-add-tag-cancel" href="$doc.getURL('view', "#${tagsId}")">$services.localization.render('core.tags.add.cancel')</a></span>
   </div>

Vulnerable code: <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>
    #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)
        ## TODO
    #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)
        #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)
        ## TODO
<form action="$doc.getURL('view', "xpage=documentTags&amp;xaction=add&amp;xredirect=${xredirect}")" method="post" class="tag-add-form">
      ## CSRF prevention
    <div class="hidden"><input type="hidden" name="form_token" value="$!{services.csrf.getToken()}" /></div>
Fixed code:   #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>
    #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")
        #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)
    #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
            #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]))
            #if("$!{request.ajax}" != '')
                $tagErrorMessage
            #elseif("$!{request.xredirect}" != '')
                $response.sendRedirect($request.xredirect)
            #end
        #else
            #set ($discard= $response.setStatus(501))
            #set ($responseMessage = "Tag plugin is missing")
        #set ($discard= $response.setStatus(401))
        #set($responseMessage = $services.localization.render('core.tags.add.error.notAllowed', [$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.
<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">