VYPR
Critical severityNVD Advisory· Published Dec 12, 2024· Updated Dec 13, 2024

XWiki allows remote code execution through the extension sheet

CVE-2024-55662

Description

XWiki Platform is a generic wiki platform. Starting in version 3.3-milestone-1 and prior to versions 15.10.9 and 16.3.0, on instances where Extension Repository Application is installed, any user can execute any code requiring programming rights on the server. This vulnerability has been fixed in XWiki 15.10.9 and 16.3.0. Since Extension Repository Application is not mandatory, it can be safely disabled on instances that do not use it as a workaround. It is also possible to manually apply the patches from commit 8659f17d500522bf33595e402391592a35a162e8 to the page ExtensionCode.ExtensionSheet and to the page ExtensionCode.ExtensionAuthorsDisplayer.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.xwiki.platform:xwiki-platform-repository-server-uiMaven
>= 3.3-milestone-1, < 15.10.915.10.9
org.xwiki.platform:xwiki-platform-repository-server-uiMaven
>= 16.0.0-rc-1, < 16.3.016.3.0

Affected products

1

Patches

1
8659f17d5005

XWIKI-21890: Improve escaping in Extension Sheet

https://github.com/xwiki/xwiki-platformPierre JeanjeanApr 5, 2024via ghsa
4 files changed · +272 25
  • xwiki-platform-core/xwiki-platform-repository/xwiki-platform-repository-server-ui/pom.xml+47 0 modified
    @@ -96,6 +96,53 @@
           <version>${rendering.version}</version>
           <scope>runtime</scope>
         </dependency>
    +    <!-- Test dependencies. -->
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-test-page</artifactId>
    +      <version>${project.version}</version>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-uiextension-api</artifactId>
    +      <version>${project.version}</version>
    +      <scope>test</scope>
    +      <type>test-jar</type>
    +    </dependency>
    +    <!-- Provides the component list for RenderingScriptService. -->
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
    +      <version>${project.version}</version>
    +      <type>test-jar</type>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-rendering-configuration-default</artifactId>
    +      <version>${project.version}</version>
    +      <type>test-jar</type>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-rendering-macro-groovy</artifactId>
    +      <version>${project.version}</version>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-logging-script</artifactId>
    +      <version>${project.version}</version>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.commons</groupId>
    +      <artifactId>xwiki-commons-logging-logback</artifactId>
    +      <version>${commons.version}</version>
    +      <scope>test</scope>
    +    </dependency>
       </dependencies>
       <build>
         <plugins>
    
  • xwiki-platform-core/xwiki-platform-repository/xwiki-platform-repository-server-ui/src/main/resources/ExtensionCode/ExtensionAuthorsDisplayer.xml+1 1 modified
    @@ -88,7 +88,7 @@
         &lt;/script&gt;
       {{/html}}
     #else
    -  #foreach($page in $pageList)#if($foreach.count &gt; 1), #end#if($xwiki.exists($page))[[${xwiki.getUserName($page, false)}&gt;&gt;$page]]#else$page#end#end
    +  #foreach($page in $pageList)#if($foreach.count &gt; 1), #end#if($xwiki.exists($page))[[${xwiki.getUserName($page, false)}&gt;&gt;$page]]#else$services.rendering.escape($page, 'xwiki/2.1')#end#end
     #end
     {{/velocity}}</content>
       <object>
    
  • xwiki-platform-core/xwiki-platform-repository/xwiki-platform-repository-server-ui/src/main/resources/ExtensionCode/ExtensionSheet.xml+28 24 modified
    @@ -90,9 +90,9 @@
         (% class="extensionSummary" %)
         #set($icon = $doc.getValue("icon"))
         #if ("$!{icon.trim()}" != "")
    -      |[[image:${doc.getValue('icon')}]]|**{{{$doc.getValue("summary")}}}**
    +      |[[image:${services.rendering.escape($doc.getValue('icon'), 'xwiki/2.1')}]]|**${services.rendering.escape($doc.getValue("summary"), 'xwiki/2.1')}**
         #else
    -      |[[image:icon:cog]]|**{{{$doc.getValue("summary")}}}**
    +      |[[image:icon:cog]]|**${services.rendering.escape($doc.getValue("summary"), 'xwiki/2.1')}**
         #end
     
         ## Viewing
    @@ -116,14 +116,14 @@
           #if ("$!typeDisplay" == '')
             #set($typeDisplay = $type)
           #end
    -      |(% class="label" width='30%' %)Type(%%)|$typeDisplay
    +      |(% class="label" width='30%' %)Type(%%)|$services.rendering.escape($typeDisplay, 'xwiki/2.1')
           ##------- Category --------------
           #set($categoryDisplay = $extensionCategoryObject.getProperty('name').value)
           #if ("$!categoryDisplay" == '')
             #set($categoryDisplay = $category)
           #end
           #if ($categoryDisplay)
    -      |(% class="label" width='30%' %)Category(%%)|$categoryDisplay
    +      |(% class="label" width='30%' %)Category(%%)|$services.rendering.escape($categoryDisplay, 'xwiki/2.1')
           #end
           ##------- Developed By --------
           #set($authors = $doc.getValue("authors"))
    @@ -134,23 +134,23 @@
             #end
           ##------- Active Installs --------
           #if ($doc.getValue('installedCount') &amp;&amp; $doc.getValue('showInstalledCount') != 0)
    -        |(% class="label" %)Active Installs(%%)|$!doc.getValue('installedCount')
    +        |(% class="label" %)Active Installs(%%)|$services.rendering.escape($!doc.getValue('installedCount'), 'xwiki/2.1')
           #end
           ##------- Rating --------
           |(% class="label" %)Rating(%%)|#displayFullRating($doc.documentReference)
           ##------- Website --------------
           #set($website = $extension.getProperty("website").value)
           #if ("$!website" != '')
             |(% class="label" %)Website(%%)|#if ($website.length() &gt; 40)
    -                                          [[{{{$website.substring(0, 40)...}}}&gt;&gt;url:$website]]
    +                                          [[$services.rendering.escape($services.rendering.escape($website.substring(0, 40), 'xwiki/2.1'), 'xwiki/2.1')...&gt;&gt;$services.rendering.escape($website, 'xwiki/2.1')]]
                                             #else
    -                                          $website
    +                                          [[$services.rendering.escape($website, 'xwiki/2.1')]]
                                             #end
           #end
           ##------- License --------
           #set($licenseName = $doc.getValue("licenseName"))
           #if ("$!licenseName" != "")
    -        |(% class="label" %)License(%%)|$licenseName
    +        |(% class="label" %)License(%%)|$services.rendering.escape($licenseName, 'xwiki/2.1')
           #else
             |(% class="label" %)License(%%)|Unknown
           #end
    @@ -178,21 +178,21 @@
               #set ($download = $lastVersionObject.getProperty("download").value)
               #if ("$!download" == '')
                 #if ($doc.getAttachment("${id}-${version}.${type}"))
    -              [[$services.icon.render('download')  Download v$version&gt;&gt;attach:${id}-${version}.${type}||class="btn btn-primary"]]##
    +              [[$services.icon.render('download')  Download v$services.rendering.escape($services.rendering.escape($version, 'xwiki/2.1'), 'xwiki/2.1')&gt;&gt;$services.rendering.escape("attach:${id}-${version}.${type}")||class="btn btn-primary"]]##
                 #end
               #else
    -            [[$services.icon.render('download')  Download v$version&gt;&gt;${download}||class="btn btn-primary"]]##
    +            [[$services.icon.render('download')  Download v$services.rendering.escape($services.rendering.escape($version, 'xwiki/2.1'), 'xwiki/2.1')&gt;&gt;$services.rendering.escape($download, 'xwiki/2.1')||class="btn btn-primary"]]##
               #end
             #end
             ##------- Source --------
             #set($source = $doc.getValue("source"))
             #if ("$!source" != "")
    -          [[Sources&gt;&gt;${source}||class="btn btn-default"]]##
    +          [[Sources&gt;&gt;$services.rendering.escape($source, 'xwiki/2.1')||class="btn btn-default"]]##
             #end
             ##------- Issues --------
             #set($issues = $doc.getValue("issueManagementURL"))
             #if ("$!issues" != "")
    -          [[Issues&gt;&gt;${issues}||class="btn btn-default"]]##
    +          [[Issues&gt;&gt;$services.rendering.escape($issues, 'xwiki/2.1')||class="btn btn-default"]]##
             #end
           )))
         {{/box}}
    @@ -240,7 +240,7 @@
         |Category|#if($proxyExtensionObject)$doc.display('category', 'view')#else$doc.display('category')#end~
         |Summary|#if($proxyExtensionObject)$doc.display('summary', 'view')#else$doc.display('summary')#end~
         ## FIXME: there seems to be a bug with custom displayer where they don't take into account the mode passed to display
    -    |Authors|#if($proxyExtensionObject)$doc.getValue('authors')#else$doc.display('authors')#end~
    +    |Authors|#if($proxyExtensionObject)$services.rendering.escape($doc.getValue('authors').toString(), 'xwiki/2.1')#else$doc.display('authors')#end~
         |License|#if($proxyExtensionObject)$doc.display('licenseName', 'view')#else$doc.display('licenseName')#end~
         |Source|$doc.display('source')
         |Display Icon Location
    @@ -255,9 +255,11 @@
         = Description =
         #if ($isViewMode)
           ## Make sure to resolve reference based on the right document
    -      $doc.getValue("description")
    +      {{context transformationContext="document" restricted=true}}
    +        $doc.getValue('description')
    +      {{/context}}
         #else
    -      $doc.display("description")
    +      $doc.display('description')
         #end
     
         #if ($extensionTypeObject &amp;&amp; $lastVersionObject &amp;&amp; "$!{extension.getProperty('customInstallationOnly').value}" != '1')
    @@ -272,7 +274,7 @@
     
           #if ($isEditMode)
             ; Custom installation only
    -        : $extension.customInstallationOnly
    +        : $extension.display('customInstallationOnly', 'edit')
           #end
           ##
           ## Standard installation
    @@ -288,9 +290,11 @@
           #if ($isEditMode)
             == Custom installation instructions
     
    -        $extension.installation
    +        $extension.display('installation', 'edit')
           #elseif ($extraInstallation != '')
    -        $extraInstallation
    +        {{context transformationContext="document" restricted=true}}
    +          $extraInstallation
    +        {{/context}}
           #end
         #end
     
    @@ -299,7 +303,7 @@
           #set ($releaseNotes = [])
           #set ($versions = $doc.getObjects("ExtensionCode.ExtensionVersionClass"))
           #foreach ($versionObject in $versions)
    -        #set ($notes = $!{versionObject.getProperty('notes').value})
    +        #set ($notes = $!{versionObject.display('notes', 'read')})
             #set ($version = $!{versionObject.getProperty('version').value})
             #if ("$!notes" != '' &amp;&amp; "$!version" != '')
               #set ($discard = $releaseNotes.add([$version, $notes]))
    @@ -310,26 +314,26 @@
           #if (!$releaseNotes.isEmpty())
             = Release Notes =
             #foreach ($entry in $releaseNotes)
    -          == v$entry.get(0) ==
    +          == v$services.rendering.escape($entry.get(0), 'xwiki/2.1') ==
               $entry.get(1)
             #end
           #end
     
    -      #set($extensionDependencies = $doc.getObjects('ExtensionCode.ExtensionDependencyClass', 'extensionVersion', $lastVersionObject.version))
    +      #set($extensionDependencies = $doc.getObjects('ExtensionCode.ExtensionDependencyClass', 'extensionVersion', $lastVersionObject.getValue('version')))
           #if ($extensionDependencies.size() &gt; 0)
             = Dependencies =
     
    -         Dependencies for this extension (${extension.getProperty('id').value} ${doc.getValue('lastVersion')}):
    +         Dependencies for this extension ($services.rendering.escape("${extension.getValue('id')} ${doc.getValue('lastVersion')}", 'xwiki/2.1')):
              #foreach($extensionDependency in $extensionDependencies)
               #set($dependencyDocumentName = $null)
               #set($dependencyDocumentNames = $services.query.xwql('from doc.object(ExtensionCode.ExtensionClass) as extension where extension.id = :id').bindValue("id", $extensionDependency.id).execute())
               #if (!$dependencyDocumentNames.isEmpty())
                 #set($dependencyDocumentName = $dependencyDocumentNames.get(0))
               #end
               #if ($dependencyDocumentName)
    -            * [[$extensionDependency.id&gt;&gt;$dependencyDocumentName]] $extensionDependency.constraint
    +            * [[$services.rendering.escape($services.rendering.escape($extensionDependency.getValue('id'), 'xwiki/2.1'), 'xwiki/2.1')&gt;&gt;$services.rendering.escape($dependencyDocumentName, 'xwiki/2.1')]] $services.rendering.escape($extensionDependency.getValue('constraint'), 'xwiki/2.1')
               #else
    -            * $extensionDependency.id $extensionDependency.constraint
    +            * $services.rendering.escape("${extensionDependency.getValue('id')} ${extensionDependency.getValue('constraint')}", 'xwiki/2.1')
               #end
             #end
           #end
    
  • xwiki-platform-core/xwiki-platform-repository/xwiki-platform-repository-server-ui/src/test/java/org/xwiki/repository/server/ui/ExtensionSheetPageTest.java+196 0 added
    @@ -0,0 +1,196 @@
    +/*
    + * See the NOTICE file distributed with this work for additional
    + * information regarding copyright ownership.
    + *
    + * This is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU Lesser General Public License as
    + * published by the Free Software Foundation; either version 2.1 of
    + * the License, or (at your option) any later version.
    + *
    + * This software is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    + * Lesser General Public License for more details.
    + *
    + * You should have received a copy of the GNU Lesser General Public
    + * License along with this software; if not, write to the Free
    + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
    + * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
    + */
    +package org.xwiki.repository.server.ui;
    +
    +import java.util.List;
    +
    +import javax.inject.Named;
    +
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
    +import org.xwiki.groovy.internal.DefaultGroovyConfiguration;
    +import org.xwiki.groovy.internal.GroovyScriptEngineFactory;
    +import org.xwiki.logging.logback.internal.DefaultLoggerManager;
    +import org.xwiki.logging.script.LoggingScriptService;
    +import org.xwiki.model.reference.DocumentReference;
    +import org.xwiki.observation.internal.DefaultObservationManager;
    +import org.xwiki.query.Query;
    +import org.xwiki.query.script.QueryManagerScriptService;
    +import org.xwiki.rendering.RenderingScriptServiceComponentList;
    +import org.xwiki.rendering.async.internal.AsyncMacro;
    +import org.xwiki.rendering.async.internal.block.DefaultBlockAsyncRenderer;
    +import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
    +import org.xwiki.rendering.internal.macro.box.DefaultBoxMacro;
    +import org.xwiki.rendering.internal.macro.context.ContextMacro;
    +import org.xwiki.rendering.internal.macro.context.ContextMacroDocument;
    +import org.xwiki.rendering.internal.macro.groovy.GroovyMacro;
    +import org.xwiki.rendering.internal.macro.script.PermissionCheckerListener;
    +import org.xwiki.rendering.internal.macro.source.DefaultMacroWikiContentSourceFactory;
    +import org.xwiki.rendering.internal.macro.toc.DefaultTocEntriesResolver;
    +import org.xwiki.rendering.internal.macro.toc.DefaultTocTreeBuilderFactory;
    +import org.xwiki.rendering.internal.macro.toc.TocMacro;
    +import org.xwiki.rendering.macro.script.MacroPermissionPolicy;
    +import org.xwiki.rendering.transformation.MacroTransformationContext;
    +import org.xwiki.script.service.ScriptService;
    +import org.xwiki.test.annotation.ComponentList;
    +import org.xwiki.test.junit5.mockito.MockComponent;
    +import org.xwiki.test.page.HTML50ComponentList;
    +import org.xwiki.test.page.PageTest;
    +import org.xwiki.test.page.TestNoScriptMacro;
    +import org.xwiki.test.page.XWikiSyntax21ComponentList;
    +
    +import com.xpn.xwiki.doc.XWikiDocument;
    +import com.xpn.xwiki.objects.BaseObject;
    +
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.when;
    +
    +/**
    + * Page Test for {@code ExtensionCode.ExtensionSheet}.
    + *
    + * @version $Id$
    + * @since 15.10.9
    + * @since 16.3.0RC1
    + */
    +@RenderingScriptServiceComponentList
    +@DefaultRenderingConfigurationComponentList
    +@HTML50ComponentList
    +@XWikiSyntax21ComponentList
    +@ComponentList({
    +    AsyncMacro.class,
    +    ContextMacroDocument.class,
    +    ContextMacro.class,
    +    DefaultBlockAsyncRenderer.class,
    +    DefaultBoxMacro.class,
    +    DefaultGroovyConfiguration.class,
    +    DefaultLoggerManager.class,
    +    DefaultMacroWikiContentSourceFactory.class,
    +    DefaultObservationManager.class,
    +    DefaultTocEntriesResolver.class,
    +    DefaultTocTreeBuilderFactory.class,
    +    GroovyMacro.class,
    +    GroovyScriptEngineFactory.class,
    +    LoggingScriptService.class,
    +    PermissionCheckerListener.class,
    +    TestNoScriptMacro.class,
    +    TocMacro.class,
    +})
    +public class ExtensionSheetPageTest extends PageTest
    +{
    +    private static final String WIKI_NAME = "xwiki";
    +
    +    private static final DocumentReference EXTENSION_SHEET =
    +        new DocumentReference(WIKI_NAME, "ExtensionCode", "ExtensionSheet");
    +
    +    private static final DocumentReference EXTENSION_CLASS =
    +        new DocumentReference(WIKI_NAME, "ExtensionCode", "ExtensionClass");
    +
    +    private static final DocumentReference EXTENSION_DEPENDENCY_CLASS =
    +        new DocumentReference(WIKI_NAME, "ExtensionCode", "ExtensionDependencyClass");
    +
    +    private static final DocumentReference EXTENSION_VERSION_CLASS =
    +        new DocumentReference(WIKI_NAME, "ExtensionCode", "ExtensionVersionClass");
    +
    +    private static final DocumentReference EXTENSION_AUTHORS_DISPLAYER =
    +        new DocumentReference(WIKI_NAME, "ExtensionCode", "ExtensionAuthorsDisplayer");
    +
    +    private static final DocumentReference TEST_PAGE =
    +        new DocumentReference(WIKI_NAME, "Test", "TestDocument");
    +
    +    private XWikiDocument testPageDocument;
    +
    +    private XWikiDocument extensionSheetDocument;
    +
    +    @MockComponent
    +    @Named("groovy")
    +    private MacroPermissionPolicy groovyMacroPermissionPolicy;
    +
    +    @BeforeEach
    +    void setUp() throws Exception
    +    {
    +        this.xwiki.initializeMandatoryDocuments(this.context);
    +        loadPage(EXTENSION_AUTHORS_DISPLAYER);
    +        loadPage(EXTENSION_DEPENDENCY_CLASS);
    +        loadPage(EXTENSION_VERSION_CLASS);
    +        loadPage(EXTENSION_CLASS);
    +        loadPage(EXTENSION_SHEET);
    +
    +        String testString = "]]}}}{{async}}{{velocity}}{{noscript /}}{{/velocity}}{{/async}}";
    +        String groovyTestString = """
    +                {{async}}
    +                {{groovy}}
    +                services.logging.getLogger("Groovy").error("SHOULD NOT BE CALLED!")
    +                {{/groovy}}
    +                {{/async}}
    +            """;
    +
    +        this.extensionSheetDocument = this.xwiki.getDocument(EXTENSION_SHEET, this.context);
    +
    +        this.testPageDocument = this.xwiki.getDocument(TEST_PAGE, this.context);
    +        this.testPageDocument.setTitle("Extension Test");
    +        BaseObject extensionObject =
    +            this.testPageDocument.newXObject(EXTENSION_CLASS, this.context);
    +        extensionObject.setStringValue("id", testString);
    +        extensionObject.setStringValue("name", testString);
    +        extensionObject.setStringValue("description", groovyTestString);
    +        extensionObject.setStringValue("summary", testString);
    +        extensionObject.setStringValue("icon", testString);
    +        extensionObject.setStringValue("type", testString);
    +        extensionObject.setStringValue("category", testString);
    +        extensionObject.setStringValue("installedCount", testString);
    +        extensionObject.setStringValue("licenseName", testString);
    +        extensionObject.setStringValue("issueManagementURL", testString);
    +        extensionObject.setStringValue("installation", groovyTestString);
    +        extensionObject.setStringValue("lastVersion", testString);
    +        extensionObject.setStringListValue("authors", List.of(testString));
    +
    +        BaseObject extensionVersionObject = this.testPageDocument.newXObject(EXTENSION_VERSION_CLASS, this.context);
    +        extensionVersionObject.setStringValue("version", testString);
    +        extensionVersionObject.setStringValue("notes", groovyTestString);
    +
    +        BaseObject extensionDependencyObject =
    +            this.testPageDocument.newXObject(EXTENSION_DEPENDENCY_CLASS, this.context);
    +        extensionDependencyObject.setStringValue("extensionVersion", testString);
    +        extensionDependencyObject.setStringValue("id", testString);
    +        extensionDependencyObject.setStringValue("constraint", testString);
    +
    +        // Mock the database.
    +        Query query = mock(Query.class);
    +        QueryManagerScriptService queryManagerScriptService =
    +            this.componentManager.registerMockComponent(ScriptService.class, "query", QueryManagerScriptService.class,
    +                false);
    +        when(queryManagerScriptService.xwql(any())).thenReturn(query);
    +        when(query.bindValue(any(), any())).thenReturn(query);
    +        when(query.execute()).thenReturn(List.of());
    +
    +        // Mock restricted contexts.
    +        when(this.groovyMacroPermissionPolicy.hasPermission(any(), any())).thenAnswer(i ->
    +            !((MacroTransformationContext) i.getArgument(1)).getTransformationContext().isRestricted());
    +    }
    +
    +    @Test
    +    void display() throws Exception
    +    {
    +        this.context.setDoc(this.testPageDocument);
    +        // The only expectation is to not get any error log due to macro executions.
    +        renderHTMLPage(this.extensionSheetDocument);
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.