CVE-2023-37914: 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 user who can view `Invitation.WebHome` can execute arbitrary script macros including Groovy and Python macros that allow remote code execution including unrestricted read and write access to all wiki contents. This vulnerability has been patched on XWiki 14.4.8, 15.2-rc-1, and 14.10.6. Users are advised to upgrade. Users unable to upgrade may manually apply the patch on `Invitation.InvitationCommon` and `Invitation.InvitationConfig`, but there are otherwise no known workarounds for this vulnerability.
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-17T18:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: XML
Files changed: xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationCommon.xml, xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationConfig.xml, xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationCommonPageTest.java, xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationConfigPageTest.java
Commit date: 2023-02-28 17:13:51 +0100
Commit message: XWIKI-20421: Improved escaping of subjectLineTemplate

Diff: diff --git a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationCommon.xml b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationCommon.xml
index a8215260b6527..ff785df5690ee 100644
--- a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationCommon.xml
+++ b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationCommon.xml
@@ -823,11 +823,11 @@
     </property>
     <property>
       <smtp_server_username/>
     </property>
     <property>
-      <subjectLineTemplate>{{velocity}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
+      <subjectLineTemplate>{{velocity wiki='false'}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
     </property>
     <property>
       <usersMayPersonalizeMessage>1</usersMayPersonalizeMessage>
     </property>
     <property>
diff --git a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationConfig.xml b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationConfig.xml
index ab60cebcf8a68..3d7172ffd1351 100644
--- a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationConfig.xml
+++ b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/main/resources/Invitation/InvitationConfig.xml
@@ -418,11 +418,11 @@
     </property>
     <property>
       <smtp_server_username/>
     </property>
     <property>
-      <subjectLineTemplate>{{velocity}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
+      <subjectLineTemplate>{{velocity wiki='false'}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
     </property>
     <property>
       <usersMayPersonalizeMessage>1</usersMayPersonalizeMessage>
     </property>
     <property>
diff --git a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationCommonPageTest.java b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationCommonPageTest.java
index b2afed39a2c8a..a7f47d967b9de 100644
--- a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationCommonPageTest.java
+++ b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationCommonPageTest.java
@@ -31,11 +31,10 @@
 import org.xwiki.model.reference.DocumentReference;
 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.InfoMessageMacro;
-import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.script.ScriptContextManager;
 import org.xwiki.security.authorization.AuthorizationManager;
 import org.xwiki.test.annotation.ComponentList;
 import org.xwiki.test.page.HTML50ComponentList;
 import org.xwiki.test.page.PageTest;
@@ -49,10 +48,13 @@
 
 import static javax.script.ScriptContext.GLOBAL_SCOPE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
+import static org.xwiki.rendering.syntax.Syntax.PLAIN_1_0;
+import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_0;
+import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_1;
 
 /**
  * Test of {@code Invitation.InvitationCommon}.
  *
  * @version $Id$
@@ -71,10 +73,13 @@
     ErrorMessageMacro.class,
     TestNoScriptMacro.class
 })
 class InvitationCommonPageTest extends PageTest
 {
+    public static final DocumentReference INVITATION_COMMON_REFERENCE =
+        new DocumentReference("xwiki", "Invitation", "InvitationCommon");
+
     private ScriptContext scriptContext;
 
     @BeforeEach
     void setUp() throws Exception
     {
@@ -89,30 +94,28 @@ void setUp() throws Exception
     void testEq0() throws Exception
     {
         this.context.setDoc(this.xwiki.getDocument(
             new DocumentReference("xwiki", "]]  {{noscript/}}", "InvitationCommon"), this.context));
 
-        DocumentReference invitationCommonReference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
-        Document document = Jsoup.parse(loadPage(invitationCommonReference).getRenderedContent(this.context));
+        Document document = Jsoup.parse(loadPage(INVITATION_COMMON_REFERENCE).getRenderedContent(this.context));
         assertEquals("xe.invitation.internalDocument []] {{noscript/}}.WebHome]",
             document.selectFirst(".infomessage").text());
     }
 
     @Test
     void testEq1ConfigClassIsNew() throws Exception
     {
         String spaceName = "<script>console.log</script>]]{{noscript/}}";
-        DocumentReference invitationCommonReference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
 
         XWikiDocument doc =
             this.xwiki.getDocument(new DocumentReference("xwiki", spaceName, "InvitationCommon"), this.context);
         this.xwiki.saveDocument(doc, this.context);
         this.context.setDoc(doc);
 
         this.request.put("test", "1");
 
-        Document document = Jsoup.parse(loadPage(invitationCommonReference).getRenderedContent(this.context));
+        Document document = Jsoup.parse(loadPage(INVITATION_COMMON_REFERENCE).getRenderedContent(this.context));
 
         assertEquals("testLoadInvitationConfig", document.selectFirst(".infomessage").text());
         Element errorMessage = document.selectFirst(".errormessage");
         assertEquals("Class document <script>console\\.log</script>]]{{noscript/}}.WebHome not found. "
             + "can't run test.", errorMessage.text());
@@ -146,11 +149,11 @@ void testEq1ConfigClassExistsInvalidFromAddress() throws Exception
         invitationConfigXObject.set("from_address", "<script>console.log('from_address')</script>", this.context);
         this.xwiki.saveDocument(hopefullyNonExistantSpaceDoc, this.context);
 
         this.request.put("test", "1");
 
-        XWikiDocument invitationCommonDoc = loadPage(new DocumentReference("xwiki", "Invitation", "InvitationCommon"));
+        XWikiDocument invitationCommonDoc = loadPage(INVITATION_COMMON_REFERENCE);
         Document document = Jsoup.parse(invitationCommonDoc.getRenderedContent(this.context));
 
         assertEquals("testLoadInvitationConfig", document.selectFirst(".infomessage").text());
         Element firstErrorMessage = document.selectFirst(".errormessage");
         assertEquals("Config map too small", firstErrorMessage.text());
@@ -185,12 +188,11 @@ void testEq1ConfigClassExistsConfigMapTooSmall() throws Exception
         invitationConfigXObject.set("from_address", "no-reply@localhost.localdomain", this.context);
         this.xwiki.saveDocument(hopefullyNonExistantSpaceDoc, this.context);
 
         this.request.put("test", "1");
 
-        DocumentReference reference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
-        XWikiDocument invitationCommonDoc = loadPage(reference);
+        XWikiDocument invitationCommonDoc = loadPage(INVITATION_COMMON_REFERENCE);
         Document document = Jsoup.parse(invitationCommonDoc.getRenderedContent(this.context));
 
         assertEquals("testLoadInvitationConfig", document.selectFirst(".infomessage").text());
         Element firstErrorMessage = document.selectFirst(".errormessage");
         assertEquals("Config map too small", firstErrorMessage.text());
@@ -227,31 +229,30 @@ void testEq1ConfigClassExistsNewInvitationConfig() throws Exception
             .forEach(value -> invitationConfigXObject.set("field" + value, "value " + value, this.context));
         this.xwiki.saveDocument(hopefullyNonExistantSpaceDoc, this.context);
 
         this.request.put("test", "1");
 
-        DocumentReference reference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
-        XWikiDocument invitationCommonDoc = loadPage(reference);
+        XWikiDocument invitationCommonDoc = loadPage(INVITATION_COMMON_REFERENCE);
         Document document = Jsoup.parse(invitationCommonDoc.getRenderedContent(this.context));
 
         assertEquals("testLoadInvitationConfig", document.selectFirst(".infomessage").text());
         Element firstErrorMessage = document.selectFirst(".errormessage");
         assertEquals("Config document not created", firstErrorMessage.text());
     }
 
     @Test
     void displayMessageVelocityMacro() throws Exception
     {
-        loadPage(new DocumentReference("xwiki", "Invitation", "InvitationCommon"));
+        loadPage(INVITATION_COMMON_REFERENCE);
         DocumentReference invitationMailClassDocumentReference =
             new DocumentReference("xwiki", "Invitation", "InvitationMailClass");
         loadPage(invitationMailClassDocumentReference);
 
         XWikiDocument page = this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Page"), this.context);
         BaseObject invitationMailXObject = page.newXObject(invitationMailClassDocumentReference, this.context);
         invitationMailXObject.set("messageBody", "<strong>message body</strong>", this.context);
-        page.setSyntax(Syntax.XWIKI_2_1);
+        page.setSyntax(XWIKI_2_1);
         page.setContent("{{include reference=\"Invitation.InvitationCommon\"/}}\n"
             + "\n"
             + "{{velocity}}\n"
             + "$mail.class\n"
             + "#displayMessage($mail)\n"
@@ -260,6 +261,24 @@ void displayMessageVelocityMacro() throws Exception
         this.scriptContext.setAttribute("mail", new Object(invitationMailXObject, this.context), GLOBAL_SCOPE);
 
         Document document = renderHTMLPage(page);
         assertEquals("<strong>message body</strong>", document.selectFirst("#preview-messagebody-field").html());
     }
+
+    /**
+     * Check if the value of subjectLine correctly escape its parameters.
+     */
+    @Test
+    void subjectLineTemplate() throws Exception
+    {
+        com.xpn.xwiki.api.Document invitationCommonDocument =
+            new com.xpn.xwiki.api.Document(loadPage(INVITATION_COMMON_REFERENCE), this.context);
+        String value = String.valueOf(
+            invitationCommonDocument.getObject("xwiki:Invitation.WebHome").getProperty("subjectLineTemplate")
+                .getValue());
+        this.oldcore.getScriptContext().setAttribute("subjectLine", "{{noscript/}}", GLOBAL_SCOPE);
+        com.xpn.xwiki.api.Document testDocument = new com.xpn.xwiki.api.Document(
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Test"), this.context), this.context);
+        String renderedContent = testDocument.getRenderedContent(value, XWIKI_2_0.toIdString(), PLAIN_1_0.toIdString());
+        assertEquals("xe.invitation.emailContent.subjectLine [XWikiGuest, null, {{noscript/}}]", renderedContent);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationConfigPageTest.java b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationConfigPageTest.java
index ab1e4a036962c..93f62fa2342d4 100644
--- a/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationConfigPageTest.java
+++ b/xwiki-platform-core/xwiki-platform-invitation/xwiki-platform-invitation-ui/src/test/java/org/xwiki/invitation/InvitationConfigPageTest.java
@@ -34,11 +34,14 @@
 import org.xwiki.test.page.TestNoScriptMacro;
 import org.xwiki.test.page.XWikiSyntax21ComponentList;
 
 import com.xpn.xwiki.doc.XWikiDocument;
 
+import static javax.script.ScriptContext.GLOBAL_SCOPE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.xwiki.rendering.syntax.Syntax.PLAIN_1_0;
+import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_0;
 
 /**
  * Test of {@code Invitation.InvitationConfig}.
  *
  * @version $Id$
@@ -57,22 +60,37 @@
     RenderingConfigClassDocumentConfigurationSource.class,
     // End - Required in additional of RenderingScriptServiceComponentList
 })
 class InvitationConfigPageTest extends PageTest
 {
-    private static final DocumentReference INVITATION_CONFIG_DOCUMENT_REFERENCE =
+    private static final DocumentReference INVITATION_CONFIG_REFERENCE =
         new DocumentReference("xwiki", "Invitation", "InvitationConfig");
 
     @Test
     void escapeInfoMessageInternalDocumentParameter() throws Exception
     {
-        XWikiDocument invitationGuestActionsDocument = loadPage(INVITATION_CONFIG_DOCUMENT_REFERENCE);
+        XWikiDocument invitationGuestActionsDocument = loadPage(INVITATION_CONFIG_REFERENCE);
 
         // Set up the current doc in the context so that $doc is bound in scripts
         this.context.setDoc(
             this.xwiki.getDocument(new DocumentReference("xwiki", "]] {{noscript/}}", "Page"), this.context));
 
         Document document = Jsoup.parse(invitationGuestActionsDocument.getRenderedContent(this.context));
         Element infomessage = document.selectFirst(".infomessage");
         assertEquals("xe.invitation.internalDocument [Invitation.WebHome]", infomessage.text());
     }
+
+    @Test
+    void subjectLineTemplate() throws Exception
+    {
+        com.xpn.xwiki.api.Document invitationConfigDocument =
+            new com.xpn.xwiki.api.Document(loadPage(INVITATION_CONFIG_REFERENCE), this.context);
+        String value = String.valueOf(
+            invitationConfigDocument.getObject("xwiki:Invitation.WebHome").getProperty("subjectLineTemplate")
+                .getValue());
+        this.oldcore.getScriptContext().setAttribute("subjectLine", "{{noscript/}}", GLOBAL_SCOPE);
+        com.xpn.xwiki.api.Document testDocument = new com.xpn.xwiki.api.Document(
+            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Test"), this.context), this.context);
+        String renderedContent = testDocument.getRenderedContent(value, XWIKI_2_0.toIdString(), PLAIN_1_0.toIdString());
+        assertEquals("xe.invitation.emailContent.subjectLine [XWikiGuest, null, {{noscript/}}]", renderedContent);
+    }
 }

Vulnerable code:       <subjectLineTemplate>{{velocity}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
      <subjectLineTemplate>{{velocity}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
import org.xwiki.rendering.syntax.Syntax;
        DocumentReference invitationCommonReference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
        Document document = Jsoup.parse(loadPage(invitationCommonReference).getRenderedContent(this.context));
        DocumentReference invitationCommonReference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
        Document document = Jsoup.parse(loadPage(invitationCommonReference).getRenderedContent(this.context));
        XWikiDocument invitationCommonDoc = loadPage(new DocumentReference("xwiki", "Invitation", "InvitationCommon"));
        DocumentReference reference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
        XWikiDocument invitationCommonDoc = loadPage(reference);
        DocumentReference reference = new DocumentReference("xwiki", "Invitation", "InvitationCommon");
        XWikiDocument invitationCommonDoc = loadPage(reference);
        loadPage(new DocumentReference("xwiki", "Invitation", "InvitationCommon"));
        page.setSyntax(Syntax.XWIKI_2_1);
    private static final DocumentReference INVITATION_CONFIG_DOCUMENT_REFERENCE =
        XWikiDocument invitationGuestActionsDocument = loadPage(INVITATION_CONFIG_DOCUMENT_REFERENCE);
Fixed code:       <subjectLineTemplate>{{velocity wiki='false'}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
      <subjectLineTemplate>{{velocity wiki='false'}}$services.localization.render('xe.invitation.emailContent.subjectLine', [$xcontext.getUser().replaceAll("^[^\.]*.", ""), $xwiki.getRequestURL().replaceAll("https?://([^/:]*).*$", "$1"), $!subjectLine]){{/velocity}}</subjectLineTemplate>
import static org.xwiki.rendering.syntax.Syntax.PLAIN_1_0;
import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_0;
import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_1;
    public static final DocumentReference INVITATION_COMMON_REFERENCE =
        new DocumentReference("xwiki", "Invitation", "InvitationCommon");

        Document document = Jsoup.parse(loadPage(INVITATION_COMMON_REFERENCE).getRenderedContent(this.context));
        Document document = Jsoup.parse(loadPage(INVITATION_COMMON_REFERENCE).getRenderedContent(this.context));
        XWikiDocument invitationCommonDoc = loadPage(INVITATION_COMMON_REFERENCE);
        XWikiDocument invitationCommonDoc = loadPage(INVITATION_COMMON_REFERENCE);
        XWikiDocument invitationCommonDoc = loadPage(INVITATION_COMMON_REFERENCE);
        loadPage(INVITATION_COMMON_REFERENCE);
        page.setSyntax(XWIKI_2_1);

    /**
     * Check if the value of subjectLine correctly escape its parameters.
     */
    @Test
    void subjectLineTemplate() throws Exception
    {
        com.xpn.xwiki.api.Document invitationCommonDocument =
            new com.xpn.xwiki.api.Document(loadPage(INVITATION_COMMON_REFERENCE), this.context);
        String value = String.valueOf(
            invitationCommonDocument.getObject("xwiki:Invitation.WebHome").getProperty("subjectLineTemplate")
                .getValue());
        this.oldcore.getScriptContext().setAttribute("subjectLine", "{{noscript/}}", GLOBAL_SCOPE);
        com.xpn.xwiki.api.Document testDocument = new com.xpn.xwiki.api.Document(
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Test"), this.context), this.context);
        String renderedContent = testDocument.getRenderedContent(value, XWIKI_2_0.toIdString(), PLAIN_1_0.toIdString());
        assertEquals("xe.invitation.emailContent.subjectLine [XWikiGuest, null, {{noscript/}}]", renderedContent);
    }
import static javax.script.ScriptContext.GLOBAL_SCOPE;
import static org.xwiki.rendering.syntax.Syntax.PLAIN_1_0;
import static org.xwiki.rendering.syntax.Syntax.XWIKI_2_0;
    private static final DocumentReference INVITATION_CONFIG_REFERENCE =
        XWikiDocument invitationGuestActionsDocument = loadPage(INVITATION_CONFIG_REFERENCE);

    @Test
    void subjectLineTemplate() throws Exception
    {
        com.xpn.xwiki.api.Document invitationConfigDocument =
            new com.xpn.xwiki.api.Document(loadPage(INVITATION_CONFIG_REFERENCE), this.context);
        String value = String.valueOf(
            invitationConfigDocument.getObject("xwiki:Invitation.WebHome").getProperty("subjectLineTemplate")
                .getValue());
        this.oldcore.getScriptContext().setAttribute("subjectLine", "{{noscript/}}", GLOBAL_SCOPE);
        com.xpn.xwiki.api.Document testDocument = new com.xpn.xwiki.api.Document(
            this.xwiki.getDocument(new DocumentReference("xwiki", "Space", "Test"), this.context), this.context);
        String renderedContent = testDocument.getRenderedContent(value, XWIKI_2_0.toIdString(), PLAIN_1_0.toIdString());
        assertEquals("xe.invitation.emailContent.subjectLine [XWikiGuest, null, {{noscript/}}]", renderedContent);
    }