CVE-2022-36095 (Other, published 2022-09-08T21:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 4.3)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
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.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/documentTags.vm
Commit message: XWIKI-19550: Wrong error message in case of missing tag plugin

  * Refactor a bit the template for tag handling


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">