CVE-2023-35152 (XML, published 2023-06-23T17:15Z)
Description: value: XWiki Platform is a generic wiki platform. Starting in version 12.9-rc-1 and prior to versions 14.4.8, 14.10.6, and 15.1, any logged in user can add dangerous content in their first name field and see it executed with programming rights. Leading to rights escalation. The vulnerability has been fixed on XWiki 14.4.8, 14.10.6, and 15.1. As a workaround, one may apply the patch manually.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-94 \u2014 Improper Control of Generation of Code ('Code Injection')
The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/pom.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/LikeManager.java
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DefaultLikeManager.java
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/script/LikeScriptService.java
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/resources/templates/likers.vm
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DefaultLikeManagerTest.java
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/script/LikeScriptServiceTest.java
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeTranslations.de.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeTranslations.es.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeTranslations.fr.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeTranslations.no.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeTranslations.ru.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeTranslations.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/LikeViewersMenuUIX.xml
xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-ui/src/main/resources/XWiki/Like/UserProfileUIX.xml
Commit message: XWIKI-17733: Use a LiveTable to display the page liked in user profile
XWIKI-17724: Display Likers in a LiveTable

  * Use livetables in Likes UI
  * Add a new missing method to count user likes globally for allowing
    pagination


Vulnerable code:
                <item>
                <item>
                <item>
import org.xwiki.component.manager.ComponentLifecycleException;
import org.xwiki.component.phase.Disposable;
public class DefaultLikeManager implements LikeManager, Initializable, Disposable
    @Override
    public void dispose() throws ComponentLifecycleException
    {
        //this.authorizationManager.
    }

#set ($likers = $services.like.getLikers($doc, 0, 50))
<div id="document-title"><h1>$services.localization.render('like.likers.title', [$doc.title])</h1></div>
<div id="likers-content">
    #if ($likers.isEmpty())
        $services.localization.render('like.likers.empty')
      <p>
          $services.localization.render('like.likers.number', [$likers.size()])
      </p>
      <ul>
          #foreach($liker in $likers)
            <li>#displayUserLink($liker)</li>
          #end
      </ul>
</div>
  
  
  
  
  
like.likers.title=Likes on {0}
</xwikidoc>
  <content/>
        <value>wiki</value>
      <async_enabled>1</async_enabled>
#set ($likedReferences = $services.like.getUserLikes($services.user.currentUserReference, 0, 50))
|=Page
#foreach ($likeReference in $likedReferences)
  #set ($likedDocument = $xwiki.getDocument($likeReference))
  |[[$likedDocument.title&gt;&gt;$services.model.serialize($likeReference)]]
#end

Fixed code:
              <item>
              <item>
              <item>
              <item>
                <code>java.method.addedToInterface</code>
                <new>method long org.xwiki.like.LikeManager::countUserLikes(org.xwiki.user.UserReference) throws org.xwiki.like.LikeException</new>
                <justification>Unstable API.</justification>
              </item>
    /**
     * Retrieve the total number of likes performed by a user.
     *
     * @param source the user who performs the likes to count.
     * @return the total number of likes performed.
     * @throws LikeException in case of problem when getting the information.
     * @since 12.9RC1
     */
    @Unstable
    long countUserLikes(UserReference source) throws LikeException;

     * @since 12.9RC1
    @Unstable
public class DefaultLikeManager implements LikeManager, Initializable
    @Override
    public long countUserLikes(UserReference source) throws LikeException
    {
        try {
            return this.ratingsManager.countRatings(
                Collections.singletonMap(RatingsManager.RatingQueryField.USER_REFERENCE, source));
        } catch (RatingsException e) {
            throw new LikeException(
                String.format("Error when trying to count user likes for user [%s]", source), e);
        }
    }

    /**
     * Count the number of likes performed by the given user.
     *
     * @param userReference the user for whom to count likes.
     * @return the number of likes performed.
     * @since 12.9RC1
     */
    @Unstable
    public Optional<Long> countUserLikes(UserReference userReference)
    {
        Optional<Long> result = Optional.empty();
        try {
            result = Optional.of(this.likeManager.countUserLikes(userReference));
        } catch (LikeException e) {
            this.logger.warn("Error while counting likes for user [{}]", userReference,
                ExceptionUtils.getRootCause(e));
        }
        return result;
    }

#if ($request.livetable == "true")
    #macro (displayUserAliasWithAvatar $userReference $disabled)
    <div class="user#if ($disabled) disabled#end" data-reference="$escapetool.xml($userReference)">
    <span class="user-avatar-wrapper">
      #getUserAvatarURL($userReference $avatarURL 120)
      <img class="user-avatar" src="$escapetool.xml($avatarURL.url)" />
    </span>
      <a href="$xwiki.getURL($userReference)">$escapetool.xml($userReference.name)</a>
    </div>
    #end
    $response.setContentType('application/json')
    #set ($documentReference = $doc.documentReference)
    ##==============================
    ## Offset = item # at which to start displaying data
    ##==============================
    #set($offset = $numbertool.toNumber($request.get('offset')))
    ## offset starts from 0 in velocity and 1 in javascript
    #set($offset = $offset - 1)
    #if($offset < 0)
        #set($offset = 0)
    #end
    ##==================
    ## Limit = # of items to display
    ##==================
    #set($limit = $numbertool.toNumber($request.get('limit')))
    ##==========
    ## Sort direction
    ##==========
    #set($order = "$!request.sort")
    #if($order != '')
        #set($orderDirection = "$!{request.get('dir').toLowerCase()}")
        #if("$!orderDirection" != '' && "$!orderDirection" != 'asc')
            #set($orderDirection = 'desc')
        #end
    #end
    #set ($likeRecords = $services.like.getLikers($documentReference, $offset, $limit))
    #set ($userRows = [])
    #foreach($userReference in $likeRecords)
        #set ($grayed = $xcontext.userReference == $userReference.reference)
        #set ($userDoc = $xwiki.getDocument($userReference.reference))
        #set ($userProperties = $services.user.getProperties($userReference))
        #set ($userObject = $user.getObject('XWiki.XWikiUsers'))
        #set ($row = {
            'grayed': $grayed,
            'doc_fullName': $userDoc.fullName,
            'doc_wiki': $userDoc.wiki,
            'doc_url': $userDoc.getURL(),
            'doc_viewable': $services.security.authorization.hasAccess('view', $userDoc.documentReference),
            'name': "#displayUserAliasWithAvatar($userDoc.documentReference $disabled)",
            'first_name': $userProperties.firstName,
            'last_name': $userProperties.lastName
        })
        #set ($discard = $userRows.add($row))
    #end
    ## ===
    ## JSON
    ## ===
    #set ($newOffset = $offset + 1)
    #set ($optLikesNumber = $services.like.getLikes($documentReference))
    #if ($optLikesNumber.isPresent())
      #set ($totalRows = $optLikesNumber.get())
      #set ($totalRows = $likeRecords.size())
    {
        "totalrows": $totalRows,
        "returnedrows":  $likeRecords.size(),
        "offset": $newOffset,
        "reqNo": $numbertool.toNumber($request.reqNo),
        "rows": $jsontool.serialize($userRows)
    }
#else
    <h1>$escapetool.xml($services.localization.render('like.likers.title', [$doc.plainTitle]))</h1>
    #set($columns = ["name", "first_name", "last_name"])
    #set($columnsProperties = {
        "name" : { "type" : "text", "sortable": false, "filterable": false, "html": true },
        "first_name" : { "type" : "text", "sortable": false, "filterable": false},
        "last_name" : { "type" : "text", "sortable": false, "filterable": false}
    })
    #set ($queryParams = {
        "livetable": "true",
        "xpage": "likers",
        "outputSyntax": "plain"
    })
    ## We rely on the same column name than the Users administration, so we use same translation prefix for now.
    #set($options = {
        'url': $doc.getURL('get', $escapetool.url($queryParams)),
        'outputOnlyHtml': true,
        'translationPrefix' : "xe.admin.users."
    })

    #livetable("likers" $columns $columnsProperties $options)
#end

    @Test
    void countUserLikes() throws Exception
    {
        when(this.ratingsManager.countRatings(
            Collections.singletonMap(RatingsManager.RatingQueryField.USER_REFERENCE, this.userReference)))
            .thenReturn(42L);

        assertEquals(42L, this.defaultLikeManager.countUserLikes(this.userReference));
    }
import java.util.List;
import static org.mockito.Mockito.mock;

    @Test
    void countUserLikes() throws LikeException
    {
        when(this.likeManager.countUserLikes(userReference)).thenReturn(43L);
        assertEquals(Optional.of(43L), this.likeScriptService.countUserLikes(this.userReference));
    }

    @Test
    void getUserLikes() throws LikeException
    {
        List<EntityReference> expectedList = Arrays.asList(
            mock(EntityReference.class),
            mock(EntityReference.class),
            mock(EntityReference.class));
        when(this.likeManager.getUserLikes(this.userReference, 2, 32)).thenReturn(expectedList);
        assertEquals(expectedList, this.likeScriptService.getUserLikes(this.userReference, 2, 32));
    }
like.likers.title=Likers of {0}
like.livetable.column.title=Title
like.livetable.column.location=Location
like.livetable.column.likes=Likes
</xwikidoc>
  <content>{{velocity}}
#if ($request.livetable == "true")
  #template('hierarchy_macros.vm')
  #if("$!{request.xpage}" == 'plain')
    $response.setContentType('application/json')
  #end
  #set ($documentReference = $services.model.resolveDocument($request.user))
  ##==============================
  ## Offset = item # at which to start displaying data
  ##==============================
  #set($offset = $numbertool.toNumber($request.get('offset')))
  ## offset starts from 0 in velocity and 1 in javascript
  #set($offset = $offset - 1)
  #if($offset &lt; 0)
    #set($offset = 0)
  #end
  ##==================
  ## Limit = # of items to display
  ##==================
  #set($limit = $numbertool.toNumber($request.get('limit')))
  #set ($likedPages = $services.like.getUserLikes($documentReference, $offset, $limit))
  #set ($optUserLikes = $services.like.countUserLikes($documentReference))
  #if ($optUserLikes.isPresent())
    #set ($totalRows = $optUserLikes.get())
  #else
    #set ($totalRows = $likedPages.size())
  #end
  ##==========
  ## Sort direction
  ##==========
  #set($order = "$!request.sort")
  #if($order != '')
    #set($orderDirection = "$!{request.get('dir').toLowerCase()}")
    #if("$!orderDirection" != '' &amp;&amp; "$!orderDirection" != 'asc')
      #set($orderDirection = 'desc')
    #end
  #end
  #set ($pagesRows = [])
  #foreach($likedPage in $likedPages)
    #set ($likedDoc = $xwiki.getDocument($likedPage))
    #set ($optDocumentLikes = $services.like.getLikes($likedPage))
    #if ($optDocumentLikes.isPresent())
      #set ($documentLikes = $optDocumentLikes.get())
    #else
      #set ($documentLikes = "N/A")
    #end
    ## code inspired from getdocuments.vm
    #set ($viewable = $xwiki.hasAccessLevel('view', $xcontext.user, $services.model.serialize($likedPage, "default")))
    #set ($row = {'doc_viewable' : $viewable})
    #if (!$viewable)
      #set ($discard = $row.put('doc_fullName', $likedDoc.fullName))
    #else
      #set ($translatedDoc = $likedDoc.translatedDocument)
      #set ($fullname = $services.model.serialize($likedDoc.documentReference, 'default'))
      #set ($discard = $row.put('doc_name', $likedDoc.documentReference.name))
      #set ($discard = $row.put('doc_fullName', $fullname))
      #set ($location = "#hierarchy($likedDoc.documentReference, {'limit': 5, 'plain': false, 'local': true, 'displayTitle': false})")
      #set ($discard = $row.put('doc_location', $location))
      #set ($discard = $row.put('doc_space', $likedDoc.space))
      #set ($discard = $row.put('doc_url', $xwiki.getURL($likedDoc)))
      #set ($discard = $row.put('doc_space_url', $xwiki.getURL($services.model.createDocumentReference($!likedDoc.wiki, $!likedDoc.space, 'WebHome'))))
      #set ($discard = $row.put('doc_wiki', $likedDoc.wiki))
      #set ($discard = $row.put('doc_wiki_url', $xwiki.getURL($services.model.resolveDocument('', 'default', $likedDoc.documentReference.extractReference('WIKI')))))
      #set ($discard = $row.put('doc_author_url', $xwiki.getURL($translatedDoc.author)))
      #set ($discard = $row.put('doc_date', $xwiki.formatDate($translatedDoc.date)))
      #set ($discard = $row.put('doc_title', $translatedDoc.plainTitle))
      #set ($rawTitle = $translatedDoc.title)
      #if ($rawTitle != $row['doc_title'])
        #set ($discard = $row.put('doc_title_raw', $rawTitle))
      #end
      #set ($discard = $row.put('doc_author', $xwiki.getUserName($translatedDoc.author, false)))
      #set ($discard = $row.put('doc_creationDate', $xwiki.formatDate($translatedDoc.creationDate)))
      #set ($discard = $row.put('doc_creator', $xwiki.getUserName($translatedDoc.creator, false)))
    #end
    #set ($discard = $row.put('like', $documentLikes))
    #set ($discard = $pagesRows.add($row))
  #end
  #set ($newOffset = $offset + 1)
  ## ===
  ## JSON
  ## ===
  {
    "totalrows": $totalRows,
    "returnedrows":  $pagesRows.size(),
    "offset": $newOffset,
    "reqNo": $numbertool.toNumber($request.reqNo),
    "rows": $jsontool.serialize($pagesRows)
  }
#end
{{/velocity}}</content>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <value>doc.reference</value>
      <async_enabled>0</async_enabled>
=$escapetool.xml($services.localization.render("like.userprofile.menu"))=
#set($columns = ["doc.title", "doc.location", "like"])
#set ($docTitleDisplayName = $escapetool.xml($services.localization.render('like.livetable.column.title')))
#set ($docLocationDisplayName = $escapetool.xml($services.localization.render('like.livetable.column.location')))
#set ($likesDisplayName = $escapetool.xml($services.localization.render('like.livetable.column.likes')))
#set($columnsProperties = {
  "doc.title" : { "displayName": $docTitleDisplayName, "type": "text", "size": 30, "link": "view", "sortable": false, "filterable": false },
  "doc.location" : { "displayName": $docLocationDisplayName, "type": "text", "size": 30, "html": true, "sortable": false, "filterable": false },
  "like" : { "displayName": $likesDisplayName, "type" : "number", "sortable": false, "filterable": false }
})

#set ($currentUser = $services.model.serialize($doc.documentReference, "default"))
#set ($pageToCallReference = $services.model.resolveDocument("XWiki.Like.UserProfileUIX"))
#set ($pageToCall = $xwiki.getDocument($pageToCallReference))
#set ($queryParams = {
  "user": $currentUser,
  "livetable": "true",
  "xpage": "plain",
  "outputSyntax": "plain"
})
#set($options = {
  'url': $pageToCall.getURL('get', $escapetool.url($queryParams))
})
#livetable("likedPages" $columns $columnsProperties $options)