CVE-2023-40177: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Any registered user can use the content field of their user profile page to execute arbitrary scripts with programming rights, thus effectively performing rights escalation. This issue is present since version 4.3M2 when AppWithinMinutes Application added support for the Content field, allowing any wiki page (including the user profile page) to use its content as an AWM Content field, which has a custom displayer that executes the content with the rights of the ``AppWithinMinutes.Content`` author, rather than the rights of the content author. The vulnerability has been fixed in XWiki 14.10.5 and 15.1RC1. The fix is in the content of the AppWithinMinutes.Content page that defines the custom displayer. By using the ``display`` script service to render the content we make sure that the proper author is used for access rights checks.
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-08-23T21:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-test/xwiki-platform-appwithinminutes-test-docker/src/test/it/org/xwiki/appwithinminutes/test/ui/DocumentFieldsIT.java, xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/Content.xml
Commit date: 2023-02-15 12:44:00 +0200
Commit message: XWIKI-19906: Render the AppWithinMinutes Content field as its author

Diff: diff --git a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-test/xwiki-platform-appwithinminutes-test-docker/src/test/it/org/xwiki/appwithinminutes/test/ui/DocumentFieldsIT.java b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-test/xwiki-platform-appwithinminutes-test-docker/src/test/it/org/xwiki/appwithinminutes/test/ui/DocumentFieldsIT.java
index f1b4a1ebd7d67..405356a4035a4 100644
--- a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-test/xwiki-platform-appwithinminutes-test-docker/src/test/it/org/xwiki/appwithinminutes/test/ui/DocumentFieldsIT.java
+++ b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-test/xwiki-platform-appwithinminutes-test-docker/src/test/it/org/xwiki/appwithinminutes/test/ui/DocumentFieldsIT.java
@@ -21,10 +21,11 @@
 
 import java.util.Arrays;
 
 import org.apache.commons.lang3.RandomStringUtils;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
 import org.xwiki.appwithinminutes.test.po.ApplicationClassEditPage;
 import org.xwiki.appwithinminutes.test.po.ApplicationCreatePage;
 import org.xwiki.appwithinminutes.test.po.ApplicationHomeEditPage;
 import org.xwiki.appwithinminutes.test.po.ApplicationHomePage;
@@ -49,18 +50,19 @@
  * @since 12.10.11
  * @since 13.4.6
  * @since 13.10RC1
  */
 @UITest(properties = {
-    // Exclude the AppWithinMinutes.ClassEditSheet and AppWithinMinutes.DynamicMessageTool from the PR checker since 
+    // Exclude the AppWithinMinutes.ClassEditSheet and AppWithinMinutes.DynamicMessageTool from the PR checker since
     // they use the groovy macro which requires PR rights.
     // TODO: Should be removed once XWIKI-20529 is closed.
     // Exclude AppWithinMinutes.LiveTableEditSheet because it calls com.xpn.xwiki.api.Document.saveWithProgrammingRights
-    "xwikiPropertiesAdditionalProperties=test.prchecker.excludePattern=.*:AppWithinMinutes\\.(ClassEditSheet|DynamicMessageTool|LiveTableEditSheet)"
-})
+    "xwikiPropertiesAdditionalProperties=test.prchecker.excludePattern=.*:AppWithinMinutes\\.(ClassEditSheet|DynamicMessageTool|LiveTableEditSheet)"})
 class DocumentFieldsIT
 {
+    private String appName = RandomStringUtils.randomAlphabetic(6);
+
     @BeforeAll
     static void beforeAll(TestUtils setup)
     {
         // By default the minimal distribution used for the tests doesn't have any rights setup. Let's create an Admin
         // user part of the Admin Group and make sure that this Admin Group has admin rights in the wiki. We could also
@@ -71,18 +73,16 @@ static void beforeAll(TestUtils setup)
         setup.createAdminUser();
         setup.loginAsAdmin();
     }
 
     @Test
+    @Order(1)
     void titleAndContent(TestUtils setup)
     {
-        setup.loginAsAdmin();
-
         // Create a new application.
-        String appName = RandomStringUtils.randomAlphabetic(6);
         ApplicationCreatePage appCreatePage = ApplicationCreatePage.gotoPage();
-        appCreatePage.setApplicationName(appName);
+        appCreatePage.setApplicationName(this.appName);
         ApplicationClassEditPage classEditPage = appCreatePage.clickNextStep();
 
         // Add a standard field.
         ClassFieldEditPane numberField = classEditPage.addField("Number");
 
@@ -124,32 +124,55 @@ void titleAndContent(TestUtils setup)
         ViewPage entryViewPage = entryEditPage.clickSaveAndView();
         assertEquals("Foo", entryViewPage.getDocumentTitle());
         assertTrue(entryViewPage.getContent().contains("Bar"));
 
         // Verify that we can edit the document fields in-place.
-        String propertyReference = String.format("%s.Code.%1$sClass[0].title1", appName);
+        String propertyReference = String.format("%s.Code.%1$sClass[0].title1", this.appName);
         EditablePropertyPane<String> titleProperty = new EditablePropertyPane<>(propertyReference);
         assertEquals("Foo", titleProperty.clickEdit().getValue());
         titleProperty.setValue("Book").clickSave();
         assertEquals("Book", titleProperty.getDisplayValue());
 
         // Check the entries live table.
-        entryViewPage.clickBreadcrumbLink(appName);
+        entryViewPage.clickBreadcrumbLink(this.appName);
         LiveTableElement liveTable = new ApplicationHomePage().getEntriesLiveTable();
         liveTable.waitUntilReady();
         assertEquals(1, liveTable.getRowCount());
         assertTrue(liveTable.hasRow("My Title", "Book"));
         assertTrue(liveTable.hasRow("My Content", "Bar"));
 
         // Check that the title and the content of the class have not been changed.
-        setup.gotoPage(new LocalDocumentReference(Arrays.asList(appName, "Code"), appName + "Class"), "edit",
+        setup.gotoPage(new LocalDocumentReference(Arrays.asList(this.appName, "Code"), this.appName + "Class"), "edit",
             "editor=wiki");
         WikiEditPage editPage = new WikiEditPage();
-        assertEquals(appName + " Class", editPage.getTitle());
+        assertEquals(this.appName + " Class", editPage.getTitle());
         assertEquals("", editPage.getContent());
 
         // Now edit the class and check if the default values for title and content are taken from the template.
         editPage.edit();
         assertEquals(defaultTitle, new ClassFieldEditPane("title1").getDefaultValue());
         assertEquals(defaultContent, new ClassFieldEditPane("content1").getDefaultValue());
     }
+
+    @Test
+    @Order(2)
+    void contentFromSimpleUser(TestUtils setup)
+    {
+        // Create an application entry with a simple user that doesn't have script rights.
+        setup.createUserAndLogin("Alice", "pass");
+
+        ApplicationHomePage appHomePage = ApplicationHomePage.gotoPage(this.appName);
+        appHomePage.getEntriesLiveTable().waitUntilReady();
+
+        EntryNamePane entryNamePage = appHomePage.clickAddNewEntry();
+        entryNamePage.setName("ByAlice");
+
+        EntryEditPage entryEditPage = entryNamePage.clickAdd();
+        entryEditPage.setTitle("Title by $services.localization.render('Alice')");
+        entryEditPage.setContent("Content by {{velocity}}$services.localization.render('Alice'){{/velocity}}");
+
+        ViewPage entryViewPage = entryEditPage.clickSaveAndView();
+        assertEquals("Title by $services.localization.render('Alice')", entryViewPage.getDocumentTitle());
+        assertTrue(entryViewPage.getContent().contains("Content by"));
+        assertTrue(entryViewPage.getContent().contains("The execution of the [velocity] script macro is not allowed"));
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/Content.xml b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/Content.xml
index 84ed295c03d7f..acadc8a6d67b1 100644
--- a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/Content.xml
+++ b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/Content.xml
@@ -18,11 +18,11 @@
  * 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.
 -->
 
-<xwikidoc version="1.2" reference="AppWithinMinutes.Content" locale="">
+<xwikidoc version="1.5" reference="AppWithinMinutes.Content" locale="">
   <web>AppWithinMinutes</web>
   <name>Content</name>
   <language/>
   <defaultLanguage/>
   <translation>0</translation>
@@ -74,13 +74,16 @@
   #else
     &lt;/div&gt;
   #end
   {{/html}}
 #elseif ("$!type" != '')
-  ## Include the content of the current document.
-  ## Escape {{ in the rendered content to be sure that the HTML macro is not closed unintentionally.
-  {{html}}$tdoc.getRenderedContent($tdoc.content, $tdoc.syntax.toIdString()).replace('{{', '&amp;amp;#123;&amp;amp;#123;'){{/html}}
+  ## Display the content of the current document without using any sheet. We can't use the include macro here (with the
+  ## author parameter) because the content may have unsaved changes (e.g. on preview action). We make sure that the HTML
+  ## macro is not closed unintentionally, even though the XHTML printer protects us against this, just to be extra safe.
+  {{html}}$services.display.content($tdoc, {
+    'displayerHint': 'default'
+  }).replace('{{/html}}', '&amp;amp;#123;&amp;amp;#123;/html&amp;amp;#125;&amp;amp;#125;'){{/html}}
 #else
   The display mode is not specified!
 #end
 {{/velocity}}</content>
   <class>

Vulnerable code:     // Exclude the AppWithinMinutes.ClassEditSheet and AppWithinMinutes.DynamicMessageTool from the PR checker since 
    "xwikiPropertiesAdditionalProperties=test.prchecker.excludePattern=.*:AppWithinMinutes\\.(ClassEditSheet|DynamicMessageTool|LiveTableEditSheet)"
})
        setup.loginAsAdmin();

        String appName = RandomStringUtils.randomAlphabetic(6);
        appCreatePage.setApplicationName(appName);
        String propertyReference = String.format("%s.Code.%1$sClass[0].title1", appName);
        entryViewPage.clickBreadcrumbLink(appName);
        setup.gotoPage(new LocalDocumentReference(Arrays.asList(appName, "Code"), appName + "Class"), "edit",
        assertEquals(appName + " Class", editPage.getTitle());
<xwikidoc version="1.2" reference="AppWithinMinutes.Content" locale="">
  ## Include the content of the current document.
  ## Escape {{ in the rendered content to be sure that the HTML macro is not closed unintentionally.
  {{html}}$tdoc.getRenderedContent($tdoc.content, $tdoc.syntax.toIdString()).replace('{{', '&amp;amp;#123;&amp;amp;#123;'){{/html}}
Fixed code: import org.junit.jupiter.api.Order;
    // Exclude the AppWithinMinutes.ClassEditSheet and AppWithinMinutes.DynamicMessageTool from the PR checker since
    "xwikiPropertiesAdditionalProperties=test.prchecker.excludePattern=.*:AppWithinMinutes\\.(ClassEditSheet|DynamicMessageTool|LiveTableEditSheet)"})
    private String appName = RandomStringUtils.randomAlphabetic(6);

    @Order(1)
        appCreatePage.setApplicationName(this.appName);
        String propertyReference = String.format("%s.Code.%1$sClass[0].title1", this.appName);
        entryViewPage.clickBreadcrumbLink(this.appName);
        setup.gotoPage(new LocalDocumentReference(Arrays.asList(this.appName, "Code"), this.appName + "Class"), "edit",
        assertEquals(this.appName + " Class", editPage.getTitle());

    @Test
    @Order(2)
    void contentFromSimpleUser(TestUtils setup)
    {
        // Create an application entry with a simple user that doesn't have script rights.
        setup.createUserAndLogin("Alice", "pass");

        ApplicationHomePage appHomePage = ApplicationHomePage.gotoPage(this.appName);
        appHomePage.getEntriesLiveTable().waitUntilReady();

        EntryNamePane entryNamePage = appHomePage.clickAddNewEntry();
        entryNamePage.setName("ByAlice");

        EntryEditPage entryEditPage = entryNamePage.clickAdd();
        entryEditPage.setTitle("Title by $services.localization.render('Alice')");
        entryEditPage.setContent("Content by {{velocity}}$services.localization.render('Alice'){{/velocity}}");

        ViewPage entryViewPage = entryEditPage.clickSaveAndView();
        assertEquals("Title by $services.localization.render('Alice')", entryViewPage.getDocumentTitle());
        assertTrue(entryViewPage.getContent().contains("Content by"));
        assertTrue(entryViewPage.getContent().contains("The execution of the [velocity] script macro is not allowed"));
    }
<xwikidoc version="1.5" reference="AppWithinMinutes.Content" locale="">
  ## Display the content of the current document without using any sheet. We can't use the include macro here (with the
  ## author parameter) because the content may have unsaved changes (e.g. on preview action). We make sure that the HTML
  ## macro is not closed unintentionally, even though the XHTML printer protects us against this, just to be extra safe.
  {{html}}$services.display.content($tdoc, {
    'displayerHint': 'default'
  }).replace('{{/html}}', '&amp;amp;#123;&amp;amp;#123;/html&amp;amp;#125;&amp;amp;#125;'){{/html}}