CVE-2021-32621 (published 2021-05-28T21:15Z)

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 v2 base score: 6.5 (AV:N/AC:L/Au:S/C:P/I:P/A:P)
CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-94
cwe_name: Improper Control of Generation of Code ('Code Injection')
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.
extended_description: 
            When a product allows a user's input to contain code syntax, it might be possible for an attacker to craft the code in such a way that it will alter the intended control flow of the product. Such an alteration could lead to arbitrary code execution.
            Injection problems encompass a wide variety of issues -- all mitigated in very different ways. For this reason, the most effective way to discuss these weaknesses is to note the distinct features which classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed. The most classic instantiations of this category of weakness are SQL injection and format string vulnerabilities.
         
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Simon Urli
author_date: 2021-01-12 15:42:14+01:00
msg: XWIKI-17794: Properly interpret velocity in gadget titles
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: DefaultGadgetSource.java
change_type: ModificationType.MODIFY
diff: @@ -44,17 +44,16 @@
 import org.xwiki.rendering.block.WordBlock;
 import org.xwiki.rendering.block.XDOM;
 import org.xwiki.rendering.executor.ContentExecutor;
-import org.xwiki.rendering.executor.ContentExecutorException;
 import org.xwiki.rendering.listener.reference.ResourceReference;
 import org.xwiki.rendering.listener.reference.ResourceType;
 import org.xwiki.rendering.macro.dashboard.Gadget;
 import org.xwiki.rendering.macro.dashboard.GadgetSource;
-import org.xwiki.rendering.parser.MissingParserException;
-import org.xwiki.rendering.parser.ParseException;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.rendering.transformation.MacroTransformationContext;
 import org.xwiki.rendering.util.ParserUtils;
 import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
 import org.xwiki.velocity.VelocityEngine;
 import org.xwiki.velocity.VelocityManager;
 
@@ -120,6 +119,9 @@ public class DefaultGadgetSource implements GadgetSource
     @Inject
     private JobProgressManager progress;
 
+    @Inject
+    private AuthorizationManager authorizationManager;
+
     /**
      * Prepare the parser to parse the title and content of the gadget into blocks.
      */
@@ -194,19 +196,23 @@ private List<Gadget> prepareGadgets(List<BaseObject> objects, Syntax sourceSynta
                 String position = xObject.getStringValue("position");
                 String id = xObject.getNumber() + "";
 
-                // 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();
+                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;
+                }
 
                 // parse both the title and content in the syntax of the transformation context
                 List<Block> titleBlocks =
                     renderGadgetProperty(gadgetTitle, sourceSyntax, xObject.getDocumentReference(),
-                        xObject.getOwnerDocument(), context);
+                        ownerDocument, context);
                 List<Block> contentBlocks =
                     renderGadgetProperty(content, sourceSyntax, xObject.getDocumentReference(),
-                        xObject.getOwnerDocument(), context);
+                        ownerDocument, context);
 
                 // create a gadget will all these and add the gadget to the container of gadgets
                 Gadget gadget = new Gadget(id, titleBlocks, contentBlocks, position);
@@ -222,6 +228,18 @@ private List<Gadget> prepareGadgets(List<BaseObject> objects, Syntax sourceSynta
         return gadgets;
     }
 
+    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());
+    }
+
     private List<Block> renderGadgetProperty(String content, Syntax sourceSyntax, EntityReference sourceReference,
         XWikiDocument ownerDocument, MacroTransformationContext context)
         throws Exception

programming_language: Java
filename: DefaultGadgetSourceTest.java
change_type: ModificationType.MODIFY
diff: @@ -41,6 +41,8 @@
 import org.xwiki.rendering.transformation.MacroTransformationContext;
 import org.xwiki.rendering.transformation.TransformationContext;
 import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
@@ -57,6 +59,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @ComponentTest
@@ -72,6 +75,9 @@
     @MockComponent
     private AuthorExecutor authorExecutor;
 
+    @MockComponent
+    private AuthorizationManager authorizationManager;
+
     @Mock
     private DocumentReference documentReference;
 
@@ -99,6 +105,11 @@
     @Mock
     private MacroTransformationContext macroTransformationContext;
 
+    @Mock
+    private VelocityEngine velocityEngine;
+
+    private ContentExecutor<MacroTransformationContext> contentExecutor;
+
     @BeforeEach
     void setup(MockitoComponentManager componentManager) throws Exception
     {
@@ -123,23 +134,14 @@ void setup(MockitoComponentManager componentManager) throws Exception
         when(transformationContext.getId()).thenReturn(transformationId);
 
         VelocityManager velocityManager = componentManager.getInstance(VelocityManager.class);
-        VelocityEngine velocityEngine = mock(VelocityEngine.class);
         when(velocityManager.getVelocityEngine()).thenReturn(velocityEngine);
-        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);
         when(authorExecutor.call(any(), eq(ownerAuthorReference), eq(ownerSourceReference))).then(invocationOnMock -> {
             Callable callable = (Callable) invocationOnMock.getArguments()[0];
             return callable.call();
         });
 
-        ContentExecutor<MacroTransformationContext> contentExecutor =
+        this.contentExecutor =
             componentManager.getInstance(ContentExecutor.TYPE_MACRO_TRANSFORMATION);
         when(contentExecutor.execute(any(), any(), any(), any())).then((Answer<XDOM>) invocationOnMock -> {
             String content = invocationOnMock.getArgument(0);
@@ -162,12 +164,50 @@ void getGadgets() throws Exception
         when(gadgetObject1.getLargeStringValue("content")).thenReturn("Some content");
         when(gadgetObject1.getStringValue("position")).thenReturn("0");
         when(gadgetObject1.getNumber()).thenReturn(42);
+        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;
+        });
 
         List<Gadget> gadgets = this.defaultGadgetSource.getGadgets(testSource, macroTransformationContext);
         assertEquals(1, gadgets.size());
         Gadget gadget = gadgets.get(0);
-        assertEquals("Gadget 1", gadget.getTitle().get(0).toString());
+        assertEquals("Evaluated velocity version of gadget 1", gadget.getTitle().get(0).toString());
         assertEquals("Some content", gadget.getContent().get(0).toString());
         assertEquals("42", gadget.getId());
+        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());
     }
 }

programming_language: Java