CVE-2023-41046 (Java, published 2023-09-01T20:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: 6.3)
Weakness: CWE-862 \u2014 Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java
xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/objects/classes/TextAreaClassTest.java
Commit message: 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


Vulnerable 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);
            }
            BaseProperty property = (BaseProperty) object.safeget(name);
                    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,
                    // 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());
                    }
                    buffer.append(context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
                        isolated, context));
                } else {
                    buffer.append(content);
                }

Fixed code:
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.ContextualAuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.xml.XMLUtils;
    private static final String FAILED_VELOCITY_EXECUTION_WARNING =
        "Failed to execute velocity code in text area property [{}]: [{}]";

            displayVelocityCode(buffer, name, prefix, object, context);
            BaseProperty<?> property = (BaseProperty<?>) object.safeget(name);
                if (contentType == ContentType.VELOCITYWIKI) {
                    content = maybeEvaluateContent(name, isolated, content, sdoc);
                }

                    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,
                    } else {
                        result = content;
                    return result;
                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
            } catch (Exception e) {
                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
            }
        }
        return 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;
    }

/*
 * 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;
    }
}