CVE-2023-35153: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Platform is a generic wiki platform. Starting in version 5.4.4 and prior to versions 14.4.8, 14.10.4, and 15.0, a stored cross-site scripting vulnerability can be exploited by users with edit rights by adding a `AppWithinMinutes.FormFieldCategoryClass` class on a page and setting the payload on the page title. Then, any user visiting `/xwiki/bin/view/AppWithinMinutes/ClassEditSheet` executes the payload. The issue has been patched in XWiki 14.4.8, 14.10.4, and 15.0. As a workaround, update `AppWithinMinutes.ClassEditSheet` with a patch.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2023-06-23T18:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/pom.xml, xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/ClassEditSheet.xml, xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/test/java/org/xwikiplatform/appwithinminutes/ClassEditSheetPageTest.java
Commit date: 2023-01-24 11:38:31 +0100
Commit message: XWIKI-20365: Improved ClassEditSheet escaping

Diff: diff --git a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/pom.xml b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/pom.xml
index ad2d9f72ec01e..8ecc666a2f8d7 100644
--- a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/pom.xml
@@ -128,7 +128,14 @@
     <!-- Used to implement drag & drop in the "Structure" (class definition) and  "Presentation" (home page) steps. -->
     <dependency>
       <groupId>org.webjars</groupId>
       <artifactId>scriptaculous</artifactId>
     </dependency>
+    <!-- Test dependencies. -->
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-test-page</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/ClassEditSheet.xml b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/ClassEditSheet.xml
index 46c072a456926..47eb7153cd225 100644
--- a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/ClassEditSheet.xml
+++ b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/main/resources/AppWithinMinutes/ClassEditSheet.xml
@@ -102,11 +102,11 @@
     #set ($categoryListStatement = 'from doc.object(AppWithinMinutes.FormFieldCategoryClass) as category order by category.priority')
     &lt;ul&gt;
     #foreach ($category in $services.query.xwql($categoryListStatement).execute())
       #set ($categoryDoc = $xwiki.getDocument($category))
       &lt;li&gt;
-        &lt;div class="category"&gt;$categoryDoc.plainTitle&lt;/div&gt;
+        &lt;div class="category"&gt;$escapetool.xml($categoryDoc.plainTitle)&lt;/div&gt;
         #set ($formFieldsForCategoryStatement = "from doc.object($formFieldClassName) as field where field.category = :category order by field.priority")
         #set ($formFieldsForCategoryQuery = $services.query.xwql($formFieldsForCategoryStatement).bindValue('category', $category))
         &lt;ul&gt;
         #foreach ($formField in $formFieldsForCategoryQuery.execute())
           #set ($formFieldDoc = $xwiki.getDocument($formField))
@@ -117,11 +117,11 @@
             #if ($formFieldIcon.contains('/'))
               #set ($formFieldIconURL = $xwiki.getSkinFile($formFieldIcon))
             #else
               #set ($formFieldIconURL = $formFieldDoc.getAttachmentURL($formFieldIcon))
             #end
-            #set ($formFieldIconRendered = "&lt;img src='$formFieldIconURL' alt='$escapetool.xml($formFieldDoc.plainTitle)' class='icon' /&gt;")
+            #set ($formFieldIconRendered = "&lt;img src='$escapetool.xml($formFieldIconURL)' alt='$escapetool.xml($formFieldDoc.plainTitle)' class='icon' /&gt;")
           #end
           &lt;li class="field"&gt;
             $formFieldIconRendered
             $escapetool.xml($formFieldDoc.plainTitle)
             ## FIXME: We should use the 'get' action instead to prevent the stats module from recording this AJAX request.
@@ -137,11 +137,11 @@
               'sheet': 'AppWithinMinutes.ClassEditSheet',
               'template': 'AppWithinMinutes.ClassTemplate',
               'field': $formFieldDoc.fullName,
               'xeditmode': 'text'
             })))
-            &lt;input type="hidden" value="$fieldURL" class="data"/&gt;
+            &lt;input type="hidden" value="$escapetool.xml($fieldURL)" class="data"/&gt;
           &lt;/li&gt;
         #end
         &lt;/ul&gt;
       &lt;/li&gt;
     #end
diff --git a/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/test/java/org/xwikiplatform/appwithinminutes/ClassEditSheetPageTest.java b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/test/java/org/xwikiplatform/appwithinminutes/ClassEditSheetPageTest.java
new file mode 100644
index 0000000000000..a7b16d45aa3f3
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-appwithinminutes/xwiki-platform-appwithinminutes-ui/src/test/java/org/xwikiplatform/appwithinminutes/ClassEditSheetPageTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.xwikiplatform.appwithinminutes;
+
+import java.util.List;
+
+import org.jsoup.nodes.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.xwiki.groovy.internal.DefaultGroovyConfiguration;
+import org.xwiki.groovy.internal.GroovyScriptEngineFactory;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.query.Query;
+import org.xwiki.query.script.QueryManagerScriptService;
+import org.xwiki.rendering.internal.macro.groovy.GroovyMacro;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.script.service.ScriptService;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.page.HTML50ComponentList;
+import org.xwiki.test.page.PageTest;
+import org.xwiki.test.page.XWikiSyntax21ComponentList;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+/**
+ * Page Test of {@code AppWithinMinutes.ClassEditSheet}.
+ *
+ * @version $Id$
+ * @since 14.4.8
+ * @since 14.10.4
+ * @since 15.0
+ */
+@HTML50ComponentList
+@XWikiSyntax21ComponentList
+@ComponentList({
+    // Start GroovyMacro 
+    GroovyMacro.class,
+    GroovyScriptEngineFactory.class,
+    DefaultGroovyConfiguration.class
+    // End GroovyMacro
+})
+class ClassEditSheetPageTest extends PageTest
+{
+    private QueryManagerScriptService queryManagerScriptService;
+
+    @Mock
+    private Query query;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        this.queryManagerScriptService =
+            this.componentManager.registerMockComponent(ScriptService.class, "query", QueryManagerScriptService.class,
+                false);
+    }
+
+    @Test
+    void displayFieldPalette() throws Exception
+    {
+        loadPage(new DocumentReference("xwiki", "AppWithinMinutes", "VelocityMacros"));
+        loadPage(new DocumentReference("xwiki", "AppWithinMinutes", "ClassEditSheet"));
+
+        when(this.queryManagerScriptService.xwql("from doc.object(AppWithinMinutes.FormFieldCategoryClass) as category "
+            + "order by category.priority")).thenReturn(this.query);
+        when(this.query.execute()).thenReturn(List.of("xwiki:XWiki.Category"));
+
+        XWikiDocument xWikiDocumentCategory =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "XWiki", "Category"), this.context);
+        xWikiDocumentCategory.setTitle("<strong>TITLE</strong>");
+        this.xwiki.saveDocument(xWikiDocumentCategory, this.context);
+
+        XWikiDocument xwikiDocument =
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Page"), this.context);
+
+        xwikiDocument.setContent("{{include reference=\"AppWithinMinutes.ClassEditSheet\" /}}\n"
+            + "\n"
+            + "{{velocity}}\n"
+            + "#displayFieldPalette()\n"
+            + "{{/velocity}}\n");
+        xwikiDocument.setSyntax(Syntax.XWIKI_2_1);
+        this.xwiki.saveDocument(xwikiDocument, this.context);
+
+        Document document = renderHTMLPage(xwikiDocument);
+
+        assertEquals("<strong>TITLE</strong>", document.selectFirst(".category").text());
+    }
+}

Vulnerable code:         &lt;div class="category"&gt;$categoryDoc.plainTitle&lt;/div&gt;
            #set ($formFieldIconRendered = "&lt;img src='$formFieldIconURL' alt='$escapetool.xml($formFieldDoc.plainTitle)' class='icon' /&gt;")
            &lt;input type="hidden" value="$fieldURL" class="data"/&gt;
Fixed code:     <!-- Test dependencies. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-test-page</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
        &lt;div class="category"&gt;$escapetool.xml($categoryDoc.plainTitle)&lt;/div&gt;
            #set ($formFieldIconRendered = "&lt;img src='$escapetool.xml($formFieldIconURL)' alt='$escapetool.xml($formFieldDoc.plainTitle)' class='icon' /&gt;")
            &lt;input type="hidden" value="$escapetool.xml($fieldURL)" class="data"/&gt;
/*
 * 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.xwikiplatform.appwithinminutes;

import java.util.List;

import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.groovy.internal.DefaultGroovyConfiguration;
import org.xwiki.groovy.internal.GroovyScriptEngineFactory;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.query.Query;
import org.xwiki.query.script.QueryManagerScriptService;
import org.xwiki.rendering.internal.macro.groovy.GroovyMacro;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.script.service.ScriptService;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.page.HTML50ComponentList;
import org.xwiki.test.page.PageTest;
import org.xwiki.test.page.XWikiSyntax21ComponentList;

import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

/**
 * Page Test of {@code AppWithinMinutes.ClassEditSheet}.
 *
 * @version $Id$
 * @since 14.4.8
 * @since 14.10.4
 * @since 15.0
 */
@HTML50ComponentList
@XWikiSyntax21ComponentList
@ComponentList({
    // Start GroovyMacro 
    GroovyMacro.class,
    GroovyScriptEngineFactory.class,
    DefaultGroovyConfiguration.class
    // End GroovyMacro
})
class ClassEditSheetPageTest extends PageTest
{
    private QueryManagerScriptService queryManagerScriptService;

    @Mock
    private Query query;

    @BeforeEach
    void setUp() throws Exception
    {
        this.queryManagerScriptService =
            this.componentManager.registerMockComponent(ScriptService.class, "query", QueryManagerScriptService.class,
                false);
    }

    @Test
    void displayFieldPalette() throws Exception
    {
        loadPage(new DocumentReference("xwiki", "AppWithinMinutes", "VelocityMacros"));
        loadPage(new DocumentReference("xwiki", "AppWithinMinutes", "ClassEditSheet"));

        when(this.queryManagerScriptService.xwql("from doc.object(AppWithinMinutes.FormFieldCategoryClass) as category "
            + "order by category.priority")).thenReturn(this.query);
        when(this.query.execute()).thenReturn(List.of("xwiki:XWiki.Category"));

        XWikiDocument xWikiDocumentCategory =
            this.xwiki.getDocument(new DocumentReference("xwiki", "XWiki", "Category"), this.context);
        xWikiDocumentCategory.setTitle("<strong>TITLE</strong>");
        this.xwiki.saveDocument(xWikiDocumentCategory, this.context);

        XWikiDocument xwikiDocument =
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Page"), this.context);

        xwikiDocument.setContent("{{include reference=\"AppWithinMinutes.ClassEditSheet\" /}}\n"
            + "\n"
            + "{{velocity}}\n"
            + "#displayFieldPalette()\n"
            + "{{/velocity}}\n");
        xwikiDocument.setSyntax(Syntax.XWIKI_2_1);
        this.xwiki.saveDocument(xwikiDocument, this.context);

        Document document = renderHTMLPage(xwikiDocument);

        assertEquals("<strong>TITLE</strong>", document.selectFirst(".category").text());
    }
}