CVE-2023-41046 (published 2023-09-01T20:15Z)

XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. It is possible in XWiki to execute Velocity code without having script right by creating an XClass with a property of type "TextArea" and content type "VelocityCode" or "VelocityWiki". For the former, the syntax of the document needs to be set the `xwiki/1.0` (this syntax doesn\'t need to be installed). In both cases, when adding the property to an object, the Velocity code is executed regardless of the rights of the author of the property (edit right is still required, though). In both cases, the code is executed with the correct context author so no privileged APIs can be accessed. However, Velocity still grants access to otherwise inaccessible data and APIs that could allow further privilege escalation. At least for "VelocityCode", this behavior is most likely very old but only since XWiki 7.2, script right is a separate right, before that version all users were allowed to execute Velocity and thus this was expected and not a security issue. This has been patched in XWiki 14.10.10 and 15.4 RC1. Users are advised to upgrade. There are no known workarounds.

CVSS v3 base score: 6.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L)

Weakness information:
cwe_id: CWE-862
cwe_name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are not applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-05-02 16:24:47+02:00
msg: XWIKI-20847/XWIKI-20848: Improve Velocity execution in TextAreaClass

* Add a unit test
* Use AuthorExecutor to set the correct context author
* Check for script right and restricted properties
* Fix a comment
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: TextAreaClass.java
change_type: ModificationType.MODIFY
diff: @@ -34,7 +34,11 @@
 import org.xwiki.edit.EditorManager;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.rendering.syntax.SyntaxContent;
+import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
 import org.xwiki.stability.Unstable;
+import org.xwiki.xml.XMLUtils;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.doc.XWikiDocument;
@@ -47,6 +51,9 @@
 
 public class TextAreaClass extends StringClass
 {
+    private static final String FAILED_VELOCITY_EXECUTION_WARNING =
+        "Failed to execute velocity code in text area property [{}]: [{}]";
+
     /**
      * Possible values for the editor meta property.
      * <p>
@@ -437,47 +444,115 @@ public void displayView(StringBuffer buffer, String name, String prefix, BaseCol
         if (contentType == ContentType.PURE_TEXT) {
             super.displayView(buffer, name, prefix, object, context);
         } else if (contentType == ContentType.VELOCITY_CODE) {
-            StringBuffer result = new StringBuffer();
-            super.displayView(result, name, prefix, object, context);
-            if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0)) {
-                buffer.append(context.getWiki().parseContent(result.toString(), context));
-            } else {
-                // Don't do anything since this mode is deprecated and not supported in the new rendering.
-                buffer.append(result);
-            }
+            displayVelocityCode(buffer, name, prefix, object, context);
         } else {
-            BaseProperty property = (BaseProperty) object.safeget(name);
+            BaseProperty<?> property = (BaseProperty<?>) object.safeget(name);
             if (property != null) {
                 String content = property.toText();
                 XWikiDocument sdoc = getObjectDocument(object, context);
 
+                if (contentType == ContentType.VELOCITYWIKI) {
+                    content = maybeEvaluateContent(name, isolated, content, sdoc);
+                }
+
                 if (sdoc != null) {
-                    if (contentType == ContentType.VELOCITYWIKI) {
-                        // Start with a pass of Velocity
-                        // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care
-                        // of that
-                        VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class);
-                        content = velocityEvaluator.evaluateVelocityNoException(content,
+                    sdoc = ensureContentAuthorIsMetadataAuthor(sdoc);
+
+                    buffer.append(
+                        context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
+                            isolated, context));
+                } else {
+                    buffer.append(XMLUtils.escapeElementText(content));
+                }
+            }
+        }
+    }
+
+    private static XWikiDocument ensureContentAuthorIsMetadataAuthor(XWikiDocument sdoc)
+    {
+        XWikiDocument result;
+
+        // Make sure the right author is used to execute the textarea
+        // Clone the document to avoid changing the cached document instance
+        if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(), sdoc.getAuthors().getContentAuthor())) {
+            result = sdoc.clone();
+            result.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor());
+        } else {
+            result = sdoc;
+        }
+
+        return result;
+    }
+
+    private String maybeEvaluateContent(String name, boolean isolated, String content, XWikiDocument sdoc)
+    {
+        if (sdoc != null) {
+            // Start with a pass of Velocity
+            // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care
+            // of that
+            AuthorExecutor authorExecutor = Utils.getComponent(AuthorExecutor.class);
+            VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class);
+            try {
+                return authorExecutor.call(() -> {
+                    String result;
+                    // Check script right inside the author executor as otherwise the context author might not be
+                    // correct.
+                    if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) {
+                        result = velocityEvaluator.evaluateVelocityNoException(content,
                             isolated ? sdoc.getDocumentReference() : null);
+                    } else {
+                        result = content;
                     }
+                    return result;
+                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
+            } catch (Exception e) {
+                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
+            }
+        }
 
-                    // Make sure the right author is used to execute the textarea
-                    // Clone the document to void messaging with the cache
-                    if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(),
-                        sdoc.getAuthors().getContentAuthor())) {
-                        sdoc = sdoc.clone();
-                        sdoc.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor());
-                    }
+        return content;
+    }
 
-                    buffer.append(context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
-                        isolated, context));
-                } else {
-                    buffer.append(content);
-                }
+    private void displayVelocityCode(StringBuffer buffer, String name, String prefix, BaseCollection object,
+        XWikiContext context)
+    {
+        StringBuffer result = new StringBuffer();
+        super.displayView(result, name, prefix, object, context);
+        XWikiDocument sdoc = getObjectDocument(object, context);
+        if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0) && sdoc != null) {
+            try {
+                Utils.getComponent(AuthorExecutor.class).call(() -> {
+                    // Check script right inside the author executor as otherwise the context author might not be
+                    // correct.
+                    if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) {
+                        buffer.append(context.getWiki().parseContent(result.toString(), context));
+                    } else {
+                        buffer.append(result);
+                    }
+                    return null;
+                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
+            } catch (Exception e) {
+                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
+                buffer.append(result);
             }
+        } else {
+            // Don't do anything since this mode is deprecated and not supported in the new rendering.
+            buffer.append(result);
         }
     }
 
+    private boolean isDocumentAuthorAllowedToEvaluateScript(XWikiDocument document)
+    {
+        boolean isAllowed = !isRestricted() && !document.isRestricted();
+
+        if (isAllowed) {
+            ContextualAuthorizationManager authorization = Utils.getComponent(ContextualAuthorizationManager.class);
+            isAllowed = authorization.hasAccess(Right.SCRIPT);
+        }
+
+        return isAllowed;
+    }
+
     private XWikiDocument getObjectDocument(BaseCollection object, XWikiContext context)
     {
         try {

programming_language: Java
filename: TextAreaClassTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,243 @@
+/*
+ * 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 com.xpn.xwiki.objects.classes;
+
+import javax.inject.Provider;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.xwiki.model.document.DocumentAuthors;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.LogCaptureExtension;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.user.GuestUserReference;
+import org.xwiki.user.SuperAdminUserReference;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.render.OldRendering;
+import com.xpn.xwiki.internal.security.authorization.DefaultAuthorExecutor;
+import com.xpn.xwiki.internal.velocity.VelocityEvaluator;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.test.MockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the {@link TextAreaClass} class.
+ *
+ * @version $Id$
+ */
+@OldcoreTest
+@ComponentList({ DefaultAuthorExecutor.class })
+class TextAreaClassTest
+{
+    private static final String PROPERTY_NAME = "test";
+
+    @InjectMockitoOldcore
+    private MockitoOldcore oldcore;
+
+    @MockComponent
+    private Provider<OldRendering> oldRenderingProvider;
+
+    @MockComponent
+    private VelocityEvaluator velocityEvaluator;
+
+    @RegisterExtension
+    private final LogCaptureExtension logCaptureExtension = new LogCaptureExtension();
+
+    @Test
+    void viewWikiText()
+    {
+        // Use a spy, so we don't need to mess around with clone-support in a mock.
+        XWikiDocument spyDocument = getSpyDocument();
+
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.WIKI_TEXT);
+        BaseObject object = new BaseObject();
+        object.setOwnerDocument(spyDocument);
+        object.setLargeStringValue(PROPERTY_NAME, "**Test bold**");
+        StringBuffer buffer = new StringBuffer();
+        String renderingResult = "<p><strong>Test bold</strong></p>";
+        doAnswer(invocationOnMock -> {
+            XWikiDocument sDoc = invocationOnMock.getArgument(3);
+            // Verify that the content author is set to the metadata author.
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+            return renderingResult;
+        }).when(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(), any(XWikiDocument.class),
+            anyBoolean(), any(XWikiContext.class));
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        verify(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(),
+            any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class));
+
+        assertEquals(renderingResult, buffer.toString());
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void viewVelocityCode(boolean allowExecution)
+    {
+        XWikiDocument spyDocument = getSpyDocument();
+        // Velocity code is only supported in XWiki 1.0.
+        spyDocument.setSyntax(Syntax.XWIKI_1_0);
+
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITY_CODE);
+
+        BaseObject object = new BaseObject();
+        object.setOwnerDocument(spyDocument);
+        String velocityCode = "#set($x = 1) $1 & 1";
+        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
+        StringBuffer buffer = new StringBuffer();
+
+        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> {
+            // Verify that the content author is set to the metadata author.
+            XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC);
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+            return allowExecution;
+        });
+
+        String renderingResult = "1 &#38; 1";
+        OldRendering oldRendering = mock();
+        String renderingInput = velocityCode.replace("&", "&#38;");
+        when(oldRendering.parseContent(renderingInput, this.oldcore.getXWikiContext()))
+            .thenReturn(renderingResult);
+        when(this.oldRenderingProvider.get()).thenReturn(oldRendering);
+
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        if (allowExecution) {
+            assertEquals(renderingResult, buffer.toString());
+        } else {
+            assertEquals(renderingInput, buffer.toString());
+            verify(oldRendering, never()).parseContent(anyString(), any());
+        }
+
+        verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void viewVelocityWiki(boolean allowExecution)
+    {
+        XWikiDocument spyDocument = getSpyDocument();
+
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI);
+
+        BaseObject object = new BaseObject();
+        object.setOwnerDocument(spyDocument);
+        String velocityCode = "#set($x = \"bold\") **$1**";
+        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
+        StringBuffer buffer = new StringBuffer();
+
+        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> {
+            // Verify that the content author is set to the metadata author.
+            XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC);
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+            return allowExecution;
+        });
+
+        String velocityResult = "**bold**";
+        when(this.velocityEvaluator.evaluateVelocityNoException(velocityCode, spyDocument.getDocumentReference()))
+            .thenReturn(velocityResult);
+
+        String renderingResult = "<p><strong>bold</strong></p>";
+
+        // Mock the rendering of the result of the Velocity code. Check that the content author is set to the
+        // metadata author.
+        doAnswer(invocationOnMock -> {
+            // Verify that the input is as expected.
+            if (allowExecution) {
+                assertEquals(velocityResult, invocationOnMock.getArgument(0));
+            } else {
+                assertEquals(velocityCode, invocationOnMock.getArgument(0));
+            }
+
+            XWikiDocument sDoc = invocationOnMock.getArgument(3);
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+
+            return renderingResult;
+        }).when(spyDocument).getRenderedContent(anyString(), same(Syntax.XWIKI_2_1), anyBoolean(),
+            any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class));
+
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        assertEquals(renderingResult, buffer.toString());
+
+        // Verify that script right was actually checked.
+        verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT);
+    }
+
+    @Test
+    void viewVelocityWikiWithoutOwnerDocument()
+    {
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI);
+
+        BaseObject object = new BaseObject();
+        String velocityCode = "1 & 2";
+        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
+        StringBuffer buffer = new StringBuffer();
+
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        assertEquals("1 &#38; 2", buffer.toString());
+
+        verify(this.oldcore.getMockContextualAuthorizationManager(), never()).hasAccess(Right.SCRIPT);
+
+        assertEquals(1, this.logCaptureExtension.size());
+        assertEquals("Error while getting the syntax corresponding to object [null]."
+                + " Defaulting to using XWiki 1.0 syntax. Internal error [NullPointerException: ]",
+            this.logCaptureExtension.getMessage(0));
+    }
+
+    private XWikiDocument getSpyDocument()
+    {
+        // Use a spy, so we don't need to mess around with clone-support in a mock.
+        XWikiDocument spyDocument = spy(new XWikiDocument(new DocumentReference("wiki", "space", "page")));
+        spyDocument.setSyntax(Syntax.XWIKI_2_1);
+        this.oldcore.getXWikiContext().setDoc(spyDocument);
+        // Add some authors to the document to verify that TextAreaClass is correctly setting the content author to
+        // the metadata author.
+        DocumentAuthors authors = spyDocument.getAuthors();
+        authors.setEffectiveMetadataAuthor(GuestUserReference.INSTANCE);
+        authors.setContentAuthor(SuperAdminUserReference.INSTANCE);
+        return spyDocument;
+    }
+}

programming_language: Java