CVE-2023-50723: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: XWiki Platform is a generic wiki platform. Starting in 2.3 and prior to versions 14.10.15, 15.5.2, and 15.7-rc-1, anyone who can edit an arbitrary wiki page in an XWiki installation can gain programming right through several cases of missing escaping in the code for displaying sections in the administration interface. This impacts the confidentiality, integrity and availability of the whole XWiki installation. Normally, all users are allowed to edit their own user profile so this should be exploitable by all users of the XWiki instance. This has been fixed in XWiki 14.10.15, 15.5.2 and 15.7RC1. The patches can be manually applied to the `XWiki.ConfigurableClassMacros` and `XWiki.ConfigurableClass` pages.
CWE description: 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.
CVSS v3 base score: 8.8
Published: 2023-12-15T19:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/pom.xml, xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ConfigurableClass.xml, xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/test/java/org/xwiki/administration/ConfigurableClassPageTest.java
Commit date: 2023-08-02 15:36:27 +0200
Commit message: XWIKI-21121: Improve handling of headings in configurable section

Diff: diff --git a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/pom.xml b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/pom.xml
index c27e2b1c7f64d..6f8b7b47a1278 100644
--- a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/pom.xml
@@ -116,10 +116,36 @@
       <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-web-templates</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-security-authorization-script</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-user-default</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-user-default</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
       <plugin>
         <groupId>org.xwiki.commons</groupId>
diff --git a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ConfigurableClass.xml b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ConfigurableClass.xml
index 378ae369ef1d8..c87781046bbaf 100644
--- a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ConfigurableClass.xml
+++ b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/main/resources/XWiki/ConfigurableClass.xml
@@ -204,11 +204,17 @@
             ## This application should not run with programming rights because it evaluates code which may not be trustworthy.
             ## Removing the next line will open a security hole.
             ## Can't use $configurableObj.display('heading', 'view', false) to have proper heading id (because of the html macro)
             ## FIXME: find a cleaner solution
             #set($void = $doc.dropPermissions())
-            == #evaluate($heading) ==
+            #if ($services.security.authorization.hasAccess("script",
+              $app.authors.effectiveMetadataAuthor, $app.documentReference) &amp;&amp; !$app.restricted)
+              #set($evaluatedHeading = "#evaluate($heading)")
+            #else
+              #set($evaluatedHeading = $heading)
+            #end
+            == $services.rendering.escape($evaluatedHeading, 'xwiki/2.1') ==
           #end
           ## Display code to execute
           #if($codeToExecute != '')
             (%class="codeToExecute"%)(((##
               $codeToExecuteResult
diff --git a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/test/java/org/xwiki/administration/ConfigurableClassPageTest.java b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/test/java/org/xwiki/administration/ConfigurableClassPageTest.java
index b402dff9cb7de..399d4b3c91a49 100644
--- a/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/test/java/org/xwiki/administration/ConfigurableClassPageTest.java
+++ b/xwiki-platform-core/xwiki-platform-administration/xwiki-platform-administration-ui/src/test/java/org/xwiki/administration/ConfigurableClassPageTest.java
@@ -23,32 +23,42 @@
 import java.util.Map;
 
 import org.jsoup.nodes.Document;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mock;
 import org.xwiki.localization.macro.internal.TranslationMacro;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.query.internal.ScriptQuery;
 import org.xwiki.query.script.QueryManagerScriptService;
 import org.xwiki.rendering.RenderingScriptServiceComponentList;
 import org.xwiki.rendering.internal.configuration.DefaultRenderingConfigurationComponentList;
 import org.xwiki.rendering.internal.macro.message.ErrorMessageMacro;
+import org.xwiki.rendering.internal.macro.message.WarningMessageMacro;
 import org.xwiki.script.service.ScriptService;
-import org.xwiki.template.script.TemplateScriptService;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.security.script.SecurityScriptServiceComponentList;
 import org.xwiki.test.annotation.ComponentList;
 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 org.xwiki.user.UserReferenceComponentList;
+import org.xwiki.user.internal.converter.DocumentUserReferenceConverter;
+import org.xwiki.user.internal.document.DocumentUserReference;
 
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.model.reference.DocumentReferenceConverter;
+import com.xpn.xwiki.objects.BaseObject;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
 
 /**
  * Page test of {@code XWiki.ConfigurableClass}.
  *
@@ -56,15 +66,19 @@
  */
 @HTML50ComponentList
 @XWikiSyntax21ComponentList
 @RenderingScriptServiceComponentList
 @DefaultRenderingConfigurationComponentList
+@SecurityScriptServiceComponentList
+@UserReferenceComponentList
 @ComponentList({
     TestNoScriptMacro.class,
     TranslationMacro.class,
     ErrorMessageMacro.class,
-    TemplateScriptService.class
+    WarningMessageMacro.class,
+    DocumentUserReferenceConverter.class,
+    DocumentReferenceConverter.class
 })
 class ConfigurableClassPageTest extends PageTest
 {
     private static final String WIKI_NAME = "xwiki";
 
@@ -75,13 +89,13 @@ class ConfigurableClassPageTest extends PageTest
 
     private static final DocumentReference CONFIGURABLE_CLASS_MACROS =
         new DocumentReference(WIKI_NAME, SPACE_NAME, "ConfigurableClassMacros");
 
     private static final DocumentReference MY_SECTION =
-        new DocumentReference(WIKI_NAME, SPACE_NAME, "]]{{noscript /}}");
+        new DocumentReference(WIKI_NAME, SPACE_NAME, "]],{{noscript /}}");
 
-    private static final String MY_SECTION_SERIALIZED = "XWiki.]]{{noscript /}}";
+    private static final String MY_SECTION_SERIALIZED = "XWiki.]],{{noscript /}}";
 
     @Mock
     private QueryManagerScriptService queryService;
 
     @Mock
@@ -114,6 +128,58 @@ void escapeHeadingForError() throws Exception
 
         Document htmlPage = renderHTMLPage(CONFIGURABLE_CLASS);
         assertEquals(String.format("admin.customize %s:", MY_SECTION_SERIALIZED),
             htmlPage.selectFirst("h1").text());
     }
+
+    @Test
+    void escapeHeading() throws Exception
+    {
+        this.request.put("section", "other");
+        when(this.query.execute()).thenReturn(List.of(MY_SECTION_SERIALIZED)).thenReturn(List.of());
+        when(this.oldcore.getMockRightService()
+            .hasAccessLevel(eq("edit"), any(), any(), any())).thenReturn(true);
+
+        XWikiDocument mySectionDoc = new XWikiDocument(MY_SECTION);
+        BaseObject object = mySectionDoc.newXObject(CONFIGURABLE_CLASS, this.context);
+        object.setStringValue("displayInCategory", "other");
+        object.setStringValue("displayInSection", "other");
+        object.setStringValue("heading", "{{noscript /}}");
+        object.set("scope", "WIKI+ALL_SPACES", this.context);
+        this.xwiki.saveDocument(mySectionDoc, this.context);
+
+        Document htmlPage = renderHTMLPage(CONFIGURABLE_CLASS);
+        assertEquals("{{noscript /}}", htmlPage.selectFirst("h2").text());
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    void checkScriptRight(boolean hasScript) throws Exception
+    {
+        this.request.put("section", "other");
+        when(this.query.execute()).thenReturn(List.of(MY_SECTION_SERIALIZED)).thenReturn(List.of());
+        when(this.oldcore.getMockRightService()
+            .hasAccessLevel(eq("edit"), any(), any(), any())).thenReturn(true);
+
+        XWikiDocument mySectionDoc = new XWikiDocument(MY_SECTION);
+        BaseObject object = mySectionDoc.newXObject(CONFIGURABLE_CLASS, this.context);
+        object.setStringValue("displayInCategory", "other");
+        object.setStringValue("displayInSection", "other");
+        String originalHeading = "$appName {{noscript /}}";
+        object.setStringValue("heading", originalHeading);
+        object.set("scope", "WIKI+ALL_SPACES", this.context);
+        DocumentReference userReference = new DocumentReference(WIKI_NAME, SPACE_NAME, "Admin");
+        mySectionDoc.getAuthors().setEffectiveMetadataAuthor(new DocumentUserReference(userReference, true));
+        this.xwiki.saveDocument(mySectionDoc, this.context);
+        when(this.oldcore.getMockAuthorizationManager().hasAccess(Right.SCRIPT,
+            userReference, mySectionDoc.getDocumentReference())).thenReturn(hasScript);
+
+        Document htmlPage = renderHTMLPage(CONFIGURABLE_CLASS);
+        String expected;
+        if (hasScript) {
+            expected = String.format("%s {{noscript /}}", MY_SECTION_SERIALIZED);
+        } else {
+            expected = originalHeading;
+        }
+        assertEquals(expected, htmlPage.selectFirst("h2").text());
+    }
 }

Vulnerable code:             == #evaluate($heading) ==
import org.xwiki.template.script.TemplateScriptService;
    TemplateScriptService.class
        new DocumentReference(WIKI_NAME, SPACE_NAME, "]]{{noscript /}}");
    private static final String MY_SECTION_SERIALIZED = "XWiki.]]{{noscript /}}";
Fixed code:     <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-web-templates</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-security-authorization-script</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-user-default</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-user-default</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
            #if ($services.security.authorization.hasAccess("script",
              $app.authors.effectiveMetadataAuthor, $app.documentReference) &amp;&amp; !$app.restricted)
              #set($evaluatedHeading = "#evaluate($heading)")
            #else
              #set($evaluatedHeading = $heading)
            #end
            == $services.rendering.escape($evaluatedHeading, 'xwiki/2.1') ==
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.xwiki.rendering.internal.macro.message.WarningMessageMacro;
import org.xwiki.security.authorization.Right;
import org.xwiki.security.script.SecurityScriptServiceComponentList;
import org.xwiki.user.UserReferenceComponentList;
import org.xwiki.user.internal.converter.DocumentUserReferenceConverter;
import org.xwiki.user.internal.document.DocumentUserReference;
import com.xpn.xwiki.internal.model.reference.DocumentReferenceConverter;
import com.xpn.xwiki.objects.BaseObject;
import static org.mockito.ArgumentMatchers.eq;
@SecurityScriptServiceComponentList
@UserReferenceComponentList
    WarningMessageMacro.class,
    DocumentUserReferenceConverter.class,
    DocumentReferenceConverter.class
        new DocumentReference(WIKI_NAME, SPACE_NAME, "]],{{noscript /}}");
    private static final String MY_SECTION_SERIALIZED = "XWiki.]],{{noscript /}}";

    @Test
    void escapeHeading() throws Exception
    {
        this.request.put("section", "other");
        when(this.query.execute()).thenReturn(List.of(MY_SECTION_SERIALIZED)).thenReturn(List.of());
        when(this.oldcore.getMockRightService()
            .hasAccessLevel(eq("edit"), any(), any(), any())).thenReturn(true);

        XWikiDocument mySectionDoc = new XWikiDocument(MY_SECTION);
        BaseObject object = mySectionDoc.newXObject(CONFIGURABLE_CLASS, this.context);
        object.setStringValue("displayInCategory", "other");
        object.setStringValue("displayInSection", "other");
        object.setStringValue("heading", "{{noscript /}}");
        object.set("scope", "WIKI+ALL_SPACES", this.context);
        this.xwiki.saveDocument(mySectionDoc, this.context);

        Document htmlPage = renderHTMLPage(CONFIGURABLE_CLASS);
        assertEquals("{{noscript /}}", htmlPage.selectFirst("h2").text());
    }

    @ParameterizedTest
    @ValueSource(booleans = {true, false})
    void checkScriptRight(boolean hasScript) throws Exception
    {
        this.request.put("section", "other");
        when(this.query.execute()).thenReturn(List.of(MY_SECTION_SERIALIZED)).thenReturn(List.of());
        when(this.oldcore.getMockRightService()
            .hasAccessLevel(eq("edit"), any(), any(), any())).thenReturn(true);

        XWikiDocument mySectionDoc = new XWikiDocument(MY_SECTION);
        BaseObject object = mySectionDoc.newXObject(CONFIGURABLE_CLASS, this.context);
        object.setStringValue("displayInCategory", "other");
        object.setStringValue("displayInSection", "other");
        String originalHeading = "$appName {{noscript /}}";
        object.setStringValue("heading", originalHeading);
        object.set("scope", "WIKI+ALL_SPACES", this.context);
        DocumentReference userReference = new DocumentReference(WIKI_NAME, SPACE_NAME, "Admin");
        mySectionDoc.getAuthors().setEffectiveMetadataAuthor(new DocumentUserReference(userReference, true));
        this.xwiki.saveDocument(mySectionDoc, this.context);
        when(this.oldcore.getMockAuthorizationManager().hasAccess(Right.SCRIPT,
            userReference, mySectionDoc.getDocumentReference())).thenReturn(hasScript);

        Document htmlPage = renderHTMLPage(CONFIGURABLE_CLASS);
        String expected;
        if (hasScript) {
            expected = String.format("%s {{noscript /}}", MY_SECTION_SERIALIZED);
        } else {
            expected = originalHeading;
        }
        assertEquals(expected, htmlPage.selectFirst("h2").text());
    }