CVE-2023-34464: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Starting in version 2.2.1 until versions 14.4.8, 14.10.5, and 15.1RC1 of org.xwiki.platform:xwiki-platform-web and any version prior to 14.4.8, 14.10.5, and 15.1.RC1 of org.xwiki.platform:xwiki-platform-web-templates, any user who can edit a document in a wiki like the user profile can create a stored cross-site scripting attack. The attack occurs by putting plain HTML code into that document and then tricking another user to visit that document with the `displaycontent` or `rendercontent` template and plain output syntax. If a user with programming rights is tricked into visiting such a URL, arbitrary actions be performed with this user's rights, impacting the confidentiality, integrity, and availability of the whole XWiki installation. This has been patched in XWiki 14.4.8, 14.10.5 and 15.1RC1 by setting the content type of the response to plain text when the output syntax is not an HTML syntax.
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-23T15:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Other
Files changed: xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/rendercontent.vm, xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/DisplaycontentPageTest.java
Commit date: 2023-02-09 17:01:21 +0100
Commit message: XWIKI-20290: Set correct content type in rendercontent.vm

* Set the content type to text/plain when the output syntax is not HTML
* Add a page test

Diff: diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/rendercontent.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/rendercontent.vm
index f4d7fe5fff981..b33c7d1435118 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/rendercontent.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/rendercontent.vm
@@ -31,11 +31,24 @@
   $services.progress.pushLevel(3)
 #else
   $services.progress.pushLevel(1)
 #end
 $services.progress.startStep('Compute the content to display')
-## Make sure we don't escape the content since we want HTML to be rendered
+## Set the content type to text/plain when the output syntax is not HTML to avoid that other content is interpreted
+## as HTML by the browser. This needs to be done before calling contentvars.vm as rendering the content can modify
+## the content type when the document, e.g., produces JSON.
+#if ($response.getContentType() != 'text/plain')
+  #set ($outputSyntax = $xwiki.getAvailableRendererSyntax($request.outputSyntax, $request.outputSyntaxVersion))
+  #if ($outputSyntax)
+    #set ($syntaxType = $outputSyntax.type.toIdString())
+    #if ($syntaxType != "xhtml" && $syntaxType != "html")
+      $response.setContentType('text/plain')
+    #end
+  #end
+#end
+## Make sure we don't escape the content since non-HTML output like JSON is produced by some documents when plain
+## output syntax is specified.
 #set ($noEscapeContent = true)
 #template("contentvars.vm")
 $services.progress.endStep()
 #if ($request.outputTitle == 'true')
   $services.progress.startStep('Compute title to display and modify rendered content accordingly')
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/DisplaycontentPageTest.java b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/DisplaycontentPageTest.java
new file mode 100644
index 0000000000000..8dd125c74f236
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/test/java/org/xwiki/web/DisplaycontentPageTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.web;
+
+import javax.inject.Inject;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.xwiki.job.script.ProgressScripService;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.template.TemplateManager;
+import org.xwiki.template.script.TemplateScriptService;
+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.XWikiException;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.web.XWikiServletResponseStub;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests the {@code displaycontent.vm} template.
+ *
+ * @version $Id$
+ */
+@HTML50ComponentList
+@XWikiSyntax21ComponentList
+@ComponentList({
+    TemplateScriptService.class,
+    ProgressScripService.class
+})
+class DisplaycontentPageTest extends PageTest
+{
+    private static final DocumentReference TEST_REFERENCE = new DocumentReference("xwiki", "space", "page");
+
+    private static final String TEMPLATE_NAME = "displaycontent.vm";
+
+    private static final String HTML_CONTENT_TYPE = "text/html; charset=utf-8";
+
+    private XWikiDocument document;
+
+    @Inject
+    private TemplateManager templateManager;
+
+    @BeforeEach
+    void setUp() throws XWikiException
+    {
+        this.document = new XWikiDocument(TEST_REFERENCE);
+        this.document.setSyntax(Syntax.XWIKI_2_1);
+        this.xwiki.saveDocument(this.document, this.context);
+        this.context.setDoc(this.document);
+
+        // Allow setting the content type in the response, so we can assert it later. The default is consistent with
+        // Utils#parseTemplate(String, boolean, XWikiContext).
+        this.response = new XWikiServletResponseStub()
+        {
+            private String contentType = HTML_CONTENT_TYPE;
+
+            @Override
+            public String getContentType()
+            {
+                return this.contentType;
+            }
+
+            @Override
+            public void setContentType(String type)
+            {
+                this.contentType = type;
+            }
+        };
+        this.context.setResponse(this.response);
+    }
+
+    @Test
+    void renderHTML() throws Exception
+    {
+        this.document.setContent("Hello World");
+        this.xwiki.saveDocument(this.document, this.context);
+
+        String result = this.templateManager.render(TEMPLATE_NAME).trim();
+        assertEquals("<p>Hello World</p>", result);
+        assertEquals(HTML_CONTENT_TYPE, this.response.getContentType());
+    }
+
+    @Test
+    void renderJSON() throws Exception
+    {
+        String json = "{\"hello\": \"world\"}";
+        this.document.setContent(json
+            + " {{velocity}}#set ($discard = $response.setContentType('application/json')){{/velocity}}");
+        this.xwiki.saveDocument(this.document, this.context);
+        this.request.put("outputSyntax", "plain");
+
+        String result = this.templateManager.render(TEMPLATE_NAME).trim();
+        assertEquals(json, result);
+        assertEquals("application/json", this.response.getContentType());
+    }
+
+    @Test
+    void renderPlain() throws Exception
+    {
+        this.document.setContent("<strong>Some HTML</strong>");
+        this.xwiki.saveDocument(this.document, this.context);
+        this.request.put("outputSyntax", "plain");
+
+        String result = this.templateManager.render(TEMPLATE_NAME).trim();
+        assertEquals(this.document.getContent(), result);
+        assertEquals("text/plain", this.response.getContentType());
+    }
+}

Vulnerable code: ## Make sure we don't escape the content since we want HTML to be rendered
Fixed code: ## Set the content type to text/plain when the output syntax is not HTML to avoid that other content is interpreted
## as HTML by the browser. This needs to be done before calling contentvars.vm as rendering the content can modify
## the content type when the document, e.g., produces JSON.
#if ($response.getContentType() != 'text/plain')
  #set ($outputSyntax = $xwiki.getAvailableRendererSyntax($request.outputSyntax, $request.outputSyntaxVersion))
  #if ($outputSyntax)
    #set ($syntaxType = $outputSyntax.type.toIdString())
    #if ($syntaxType != "xhtml" && $syntaxType != "html")
      $response.setContentType('text/plain')
    #end
  #end
#end
## Make sure we don't escape the content since non-HTML output like JSON is produced by some documents when plain
## output syntax is specified.
/*
 * 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.web;

import javax.inject.Inject;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.job.script.ProgressScripService;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.template.TemplateManager;
import org.xwiki.template.script.TemplateScriptService;
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.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.web.XWikiServletResponseStub;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * Tests the {@code displaycontent.vm} template.
 *
 * @version $Id$
 */
@HTML50ComponentList
@XWikiSyntax21ComponentList
@ComponentList({
    TemplateScriptService.class,
    ProgressScripService.class
})
class DisplaycontentPageTest extends PageTest
{
    private static final DocumentReference TEST_REFERENCE = new DocumentReference("xwiki", "space", "page");

    private static final String TEMPLATE_NAME = "displaycontent.vm";

    private static final String HTML_CONTENT_TYPE = "text/html; charset=utf-8";

    private XWikiDocument document;

    @Inject
    private TemplateManager templateManager;

    @BeforeEach
    void setUp() throws XWikiException
    {
        this.document = new XWikiDocument(TEST_REFERENCE);
        this.document.setSyntax(Syntax.XWIKI_2_1);
        this.xwiki.saveDocument(this.document, this.context);
        this.context.setDoc(this.document);

        // Allow setting the content type in the response, so we can assert it later. The default is consistent with
        // Utils#parseTemplate(String, boolean, XWikiContext).
        this.response = new XWikiServletResponseStub()
        {
            private String contentType = HTML_CONTENT_TYPE;

            @Override
            public String getContentType()
            {
                return this.contentType;
            }

            @Override
            public void setContentType(String type)
            {
                this.contentType = type;
            }
        };
        this.context.setResponse(this.response);
    }

    @Test
    void renderHTML() throws Exception
    {
        this.document.setContent("Hello World");
        this.xwiki.saveDocument(this.document, this.context);

        String result = this.templateManager.render(TEMPLATE_NAME).trim();
        assertEquals("<p>Hello World</p>", result);
        assertEquals(HTML_CONTENT_TYPE, this.response.getContentType());
    }

    @Test
    void renderJSON() throws Exception
    {
        String json = "{\"hello\": \"world\"}";
        this.document.setContent(json
            + " {{velocity}}#set ($discard = $response.setContentType('application/json')){{/velocity}}");
        this.xwiki.saveDocument(this.document, this.context);
        this.request.put("outputSyntax", "plain");

        String result = this.templateManager.render(TEMPLATE_NAME).trim();
        assertEquals(json, result);
        assertEquals("application/json", this.response.getContentType());
    }

    @Test
    void renderPlain() throws Exception
    {
        this.document.setContent("<strong>Some HTML</strong>");
        this.xwiki.saveDocument(this.document, this.context);
        this.request.put("outputSyntax", "plain");

        String result = this.templateManager.render(TEMPLATE_NAME).trim();
        assertEquals(this.document.getContent(), result);
        assertEquals("text/plain", this.response.getContentType());
    }
}