CVE-2024-31997 (Java, published 2024-04-10T22:15Z)
Description: value: XWiki Platform is a generic wiki platform. Prior to versions 4.10.19, 15.5.4, and 15.10-rc-1, parameters of UI extensions are always interpreted as Velocity code and executed with programming rights. Any user with edit right on any document like the user's own profile can create UI extensions. This allows remote code execution and thereby impacts the confidentiality, integrity and availability of the whole XWiki installation. This vulnerability has been patched in XWiki 14.10.19, 15.5.4 and 15.9-RC1. No known workarounds are available.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/main/java/org/xwiki/uiextension/internal/WikiUIExtensionComponentBuilder.java
xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/main/java/org/xwiki/uiextension/internal/WikiUIExtensionParameters.java
xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/test/java/org/xwiki/uiextension/WikiUIExtensionComponentBuilderTest.java
xwiki-platform-core/xwiki-platform-uiextension/xwiki-platform-uiextension-api/src/test/java/org/xwiki/uiextension/WikiUIExtensionParametersTest.java
Commit message: XWIKI-21335: Execute UI Extension parameters with the rights of their author

* Check script right for the UI extension's author
* Use AuthorExecutor to set the context author


Vulnerable code:
        String rawParameters = baseObject.getStringValue(PARAMETERS_PROPERTY);

        WikiUIExtensionParameters parameters =
            new WikiUIExtensionParameters(id, rawParameters, this.wikiComponentManager);
import java.util.HashMap;
     * @param id the unique identifier of this set of parameters, mostly used to isolate parameters value execution
     * @param rawParameters raw parameters, their values can contain velocity directives
    public WikiUIExtensionParameters(String id, String rawParameters, ComponentManager cm)
        this.id = id;
        this.parameters = parseParameters(rawParameters);
        boolean isCacheValid = false;
            isCacheValid = true;
        }

        if (!isCacheValid) {
            this.evaluatedParameters = new HashMap<>();

            if (this.parameters.size() > 0) {
                    VelocityEngine velocityEngine = this.velocityManager.getVelocityEngine();
                    VelocityContext velocityContext = this.velocityManager.getVelocityContext();
                    for (String propertyKey : this.parameters.stringPropertyNames()) {
                        if (!StringUtils.isBlank(propertyKey)) {
                            String propertyValue = this.parameters.getProperty(propertyKey);
                                this.evaluatedParameters.put(propertyKey, writer.toString());
                        }
                    }
                } catch (XWikiVelocityException ex) {
                this.previousContextId = currentContextId;
                this.previousWiki = currentWiki;
        return this.evaluatedParameters;
import static org.junit.jupiter.api.Assertions.assertNull;
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters("id", "", this.componentManager);
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters("id", "key=value", this.componentManager);
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));

        WikiUIExtensionParameters parameters =
            new WikiUIExtensionParameters("id", paramsStr, this.componentManager);
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));

        WikiUIExtensionParameters parameters =
            new WikiUIExtensionParameters("id", paramsStr, this.componentManager);
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters("id", "key=value", this.componentManager);
        // It should fail and put a warn in the logs
        assertNull(parameters.get().get("key"));
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters("id", "key=value", this.componentManager);
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters("id", "key=value", this.componentManager);
        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("wiki1"));
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters("id", "key=value", this.componentManager);

Fixed code:
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(baseObject, this.wikiComponentManager);
import java.util.function.Function;
import java.util.stream.Collectors;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import com.xpn.xwiki.objects.BaseObject;

import static org.xwiki.uiextension.internal.WikiUIExtensionConstants.ID_PROPERTY;
import static org.xwiki.uiextension.internal.WikiUIExtensionConstants.PARAMETERS_PROPERTY;

    private AuthorExecutor authorExecutor;

    private final DocumentReference documentReference;

    private final DocumentReference authorReference;

    private final AuthorizationManager authorizationManager;

     * @param baseObject the object from which the parameters shall be loaded
    public WikiUIExtensionParameters(BaseObject baseObject, ComponentManager cm)
        this.id = baseObject.getStringValue(ID_PROPERTY);
        this.parameters = parseParameters(baseObject.getStringValue(PARAMETERS_PROPERTY));

        this.documentReference = baseObject.getDocumentReference();
        this.authorReference = baseObject.getOwnerDocument().getAuthorReference();
            this.authorExecutor = cm.getInstance(AuthorExecutor.class);
            this.authorizationManager = cm.getInstance(AuthorizationManager.class);
        Map<String, String> result;
            result = this.evaluatedParameters;
        } else {
            result = this.parameters.stringPropertyNames().stream()
                .filter(StringUtils::isNotBlank)
                .collect(Collectors.toMap(Function.identity(), this.parameters::getProperty));

            if (!this.parameters.isEmpty()
                && this.authorizationManager.hasAccess(Right.SCRIPT, this.authorReference, this.documentReference))
            {
                    this.authorExecutor.call(() -> {
                        VelocityEngine velocityEngine = this.velocityManager.getVelocityEngine();
                        VelocityContext velocityContext = this.velocityManager.getVelocityContext();
                        result.replaceAll((propertyKey, propertyValue) -> {
                                return writer.toString();

                            return propertyValue;
                        });

                        return null;
                    }, this.authorReference, this.documentReference);
                } catch (Exception ex) {

            this.evaluatedParameters = result;
            this.previousContextId = currentContextId;
            this.previousWiki = currentWiki;
        return result;
import java.util.concurrent.Callable;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
    @MockComponent
    private AuthorExecutor authorExecutor;

    @MockComponent
    private AuthorizationManager authorizationManager;

        when(this.authorExecutor.call(any(), any(), any())).thenAnswer(invocation -> {
            Callable<?> callable = invocation.getArgument(0);
            return callable.call();
        });

        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOC_REF)).thenReturn(true);

        when(extensionObject.getDocumentReference()).thenReturn(DOC_REF);
import java.util.concurrent.Callable;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.uiextension.internal.WikiUIExtensionConstants;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verifyNoInteractions;
    private static final DocumentReference AUTHOR_REFERENCE = new DocumentReference("xwiki", "XWiki", "XWikiAdmin");

    private static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("xwiki", "Space", "UIX");

    @MockComponent
    private AuthorExecutor authorExecutor;

    @MockComponent
    private AuthorizationManager authorizationManager;


        when(this.authorExecutor.call(any(), any(), any())).thenAnswer(invocation -> {
            Callable<?> callable = invocation.getArgument(0);
            return callable.call();
        });

        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE)).thenReturn(true);

        when(this.modelContext.getCurrentEntityReference()).thenReturn(new WikiReference("xwiki"));
        BaseObject mockUIX = constructMockUIXObject("");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        BaseObject mockUIX = constructMockUIXObject(paramsStr);
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        BaseObject mockUIX = constructMockUIXObject(paramsStr);
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        // It put a warning in the logs and return the not-evaluated value.
        assertEquals("value", parameters.get().get("key"));
        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);
        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);

    @Test
    void getParametersWithoutScriptRight() throws Exception
    {
        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE)).thenReturn(false);
        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);

        assertEquals("value", parameters.get().get("key"));
        verifyNoInteractions(this.velocityEngine);
    }

    @Test
    void getParametersWithAuthorExecutor() throws Exception
    {
        // Do not call the callable to check that the call to the Velocity engine is inside the author executor.
        doReturn(null).when(this.authorExecutor).call(any(), any(), any());

        BaseObject mockUIX = constructMockUIXObject("key=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);

        assertEquals("value", parameters.get().get("key"));
        verify(this.authorExecutor).call(any(), eq(AUTHOR_REFERENCE), eq(DOCUMENT_REFERENCE));
        verifyNoInteractions(this.velocityEngine);
    }

    @Test
    void getParametersWithEmptyKey() throws Exception
    {
        BaseObject mockUIX = constructMockUIXObject("=value");
        WikiUIExtensionParameters parameters = new WikiUIExtensionParameters(mockUIX, this.componentManager);

        assertTrue(parameters.get().isEmpty());
    }

    private BaseObject constructMockUIXObject(String parameters)
    {
        BaseObject result = mock();

        when(result.getStringValue(WikiUIExtensionConstants.ID_PROPERTY)).thenReturn("id");
        when(result.getStringValue(WikiUIExtensionConstants.PARAMETERS_PROPERTY)).thenReturn(parameters);
        when(result.getOwnerDocument()).thenReturn(mock(XWikiDocument.class));
        when(result.getOwnerDocument().getAuthorReference()).thenReturn(WikiUIExtensionParametersTest.AUTHOR_REFERENCE);
        when(result.getDocumentReference()).thenReturn(WikiUIExtensionParametersTest.DOCUMENT_REFERENCE);

        return result;
    }