CVE-2021-32621 (Java, published 2021-05-28T21:15Z)
Description: value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. In versions prior to 12.6.7 and 12.10.3, a user without Script or Programming right is able to execute script requiring privileges by editing gadget titles in the dashboard. The issue has been patched in XWiki 12.6.7, 12.10.3 and 13.0RC1.
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-94 \u2014 Improper Control of Generation of Code ('Code Injection')
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.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-dashboard/xwiki-platform-dashboard-macro/src/main/java/org/xwiki/rendering/internal/macro/dashboard/DefaultGadgetSource.java
xwiki-platform-core/xwiki-platform-dashboard/xwiki-platform-dashboard-macro/src/test/java/org/xwiki/rendering/internal/macro/dashboard/DefaultGadgetSourceTest.java
Commit message: XWIKI-17794: Properly interpret velocity in gadget titles


Vulnerable code:
import org.xwiki.rendering.executor.ContentExecutorException;
import org.xwiki.rendering.parser.MissingParserException;
import org.xwiki.rendering.parser.ParseException;
                // render title with velocity
                StringWriter writer = new StringWriter();
                // FIXME: the engine has an issue with $ and # as last character. To test and fix if it happens
                velocityEngine.evaluate(velocityContext, writer, key, title);
                String gadgetTitle = writer.toString();
                        xObject.getOwnerDocument(), context);
                        xObject.getOwnerDocument(), context);
        VelocityEngine velocityEngine = mock(VelocityEngine.class);
        when(velocityEngine.evaluate(any(), any(), any(), any(String.class))).then((Answer<Void>) invocation -> {
            Object[] args = invocation.getArguments();
            StringWriter stringWriter = (StringWriter) args[1];
            String title = (String) args[3];
            stringWriter.append(title);
            return null;
        });
        AuthorExecutor authorExecutor = componentManager.getInstance(AuthorExecutor.class);
        ContentExecutor<MacroTransformationContext> contentExecutor =
        assertEquals("Gadget 1", gadget.getTitle().get(0).toString());

Fixed code:
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
    @Inject
    private AuthorizationManager authorizationManager;

                String gadgetTitle;

                XWikiDocument ownerDocument = xObject.getOwnerDocument();
                if (this.authorizationManager.hasAccess(Right.SCRIPT, ownerDocument.getAuthorReference(), ownerDocument.getDocumentReference())) {
                    gadgetTitle =
                        this.evaluateVelocityTitle(velocityContext, velocityEngine, key, title, ownerDocument);
                } else {
                    gadgetTitle = title;
                }
                        ownerDocument, context);
                        ownerDocument, context);
    private String evaluateVelocityTitle(VelocityContext velocityContext, VelocityEngine velocityEngine, String key,
        String title, XWikiDocument ownerDocument) throws Exception
    {
        return this.authorExecutor.call(() -> {
            // render title with velocity
            StringWriter writer = new StringWriter();
            // FIXME: the engine has an issue with $ and # as last character. To test and fix if it happens
            velocityEngine.evaluate(velocityContext, writer, key, title);
            return writer.toString();
        }, ownerDocument.getAuthorReference(), ownerDocument.getDocumentReference());
    }

import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import static org.mockito.Mockito.verify;
    @MockComponent
    private AuthorizationManager authorizationManager;

    @Mock
    private VelocityEngine velocityEngine;

    private ContentExecutor<MacroTransformationContext> contentExecutor;

        this.contentExecutor =
        when(this.authorizationManager.hasAccess(Right.SCRIPT, ownerAuthorReference, ownerSourceReference)).thenReturn(true);
        when(this.velocityEngine.evaluate(any(), any(), any(), eq("Gadget 1"))).then((Answer<Void>) invocation -> {
            Object[] args = invocation.getArguments();
            StringWriter stringWriter = (StringWriter) args[1];
            String title = "Evaluated velocity version of gadget 1";
            stringWriter.append(title);
            return null;
        });
        assertEquals("Evaluated velocity version of gadget 1", gadget.getTitle().get(0).toString());
        verify(this.contentExecutor)
            .execute(eq("Evaluated velocity version of gadget 1"), any(), any(), any());
        verify(this.contentExecutor)
            .execute(eq("Some content"), any(), any(), any());
    }

    @Test
    void getGadgetWithoutScriptRight() throws Exception
    {
        assertEquals(new ArrayList<>(), this.defaultGadgetSource.getGadgets(testSource, macroTransformationContext));

        BaseObject gadgetObject1 = mock(BaseObject.class);
        when(xWikiDocument.getXObjects(gadgetClassReference)).thenReturn(Collections.singletonList(gadgetObject1));
        when(gadgetObject1.getOwnerDocument()).thenReturn(ownerDocument);
        when(gadgetObject1.getStringValue("title")).thenReturn("Gadget 2");
        when(gadgetObject1.getLargeStringValue("content")).thenReturn("Some other content");
        when(gadgetObject1.getStringValue("position")).thenReturn("2");
        when(gadgetObject1.getNumber()).thenReturn(12);
        when(this.authorizationManager.hasAccess(Right.SCRIPT, ownerAuthorReference, ownerSourceReference)).thenReturn(false);

        List<Gadget> gadgets = this.defaultGadgetSource.getGadgets(testSource, macroTransformationContext);
        assertEquals(1, gadgets.size());
        Gadget gadget = gadgets.get(0);
        assertEquals("Gadget 2", gadget.getTitle().get(0).toString());
        assertEquals("Some other content", gadget.getContent().get(0).toString());
        assertEquals("12", gadget.getId());
        verify(this.contentExecutor)
            .execute(eq("Gadget 2"), any(), any(), any());
        verify(this.contentExecutor)
            .execute(eq("Some other content"), any(), any(), any());