XWiki allows remote code execution through the extension sheet
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.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-repository-server-uiMaven | >= 3.3-milestone-1, < 15.10.9 | 15.10.9 |
org.xwiki.platform:xwiki-platform-repository-server-uiMaven | >= 16.0.0-rc-1, < 16.3.0 | 16.3.0 |
Affected products
1- Range: >= 3.3-milestone-1, < 15.10.9
Patches
18659f17d5005XWIKI-21890: Improve escaping in Extension Sheet
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 @@ </script> {{/html}} #else - #foreach($page in $pageList)#if($foreach.count > 1), #end#if($xwiki.exists($page))[[${xwiki.getUserName($page, false)}>>$page]]#else$page#end#end + #foreach($page in $pageList)#if($foreach.count > 1), #end#if($xwiki.exists($page))[[${xwiki.getUserName($page, false)}>>$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') && $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() > 40) - [[{{{$website.substring(0, 40)...}}}>>url:$website]] + [[$services.rendering.escape($services.rendering.escape($website.substring(0, 40), 'xwiki/2.1'), 'xwiki/2.1')...>>$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>>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')>>$services.rendering.escape("attach:${id}-${version}.${type}")||class="btn btn-primary"]]## #end #else - [[$services.icon.render('download') Download v$version>>${download}||class="btn btn-primary"]]## + [[$services.icon.render('download') Download v$services.rendering.escape($services.rendering.escape($version, 'xwiki/2.1'), 'xwiki/2.1')>>$services.rendering.escape($download, 'xwiki/2.1')||class="btn btn-primary"]]## #end #end ##------- Source -------- #set($source = $doc.getValue("source")) #if ("$!source" != "") - [[Sources>>${source}||class="btn btn-default"]]## + [[Sources>>$services.rendering.escape($source, 'xwiki/2.1')||class="btn btn-default"]]## #end ##------- Issues -------- #set($issues = $doc.getValue("issueManagementURL")) #if ("$!issues" != "") - [[Issues>>${issues}||class="btn btn-default"]]## + [[Issues>>$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 && $lastVersionObject && "$!{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" != '' && "$!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() > 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>>$dependencyDocumentName]] $extensionDependency.constraint + * [[$services.rendering.escape($services.rendering.escape($extensionDependency.getValue('id'), 'xwiki/2.1'), 'xwiki/2.1')>>$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- github.com/advisories/GHSA-j2pq-22jj-4pm5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-55662ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/8659f17d500522bf33595e402391592a35a162e8ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-j2pq-22jj-4pm5ghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-21890ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.