CVE-2023-46242: Cross-Site Request Forgery (CSRF) (CWE-352)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. In affected versions it's possible to execute a content with the right of any user via a crafted URL. A user must have `programming` privileges in order to exploit this vulnerability. This issue has been patched in XWiki 14.10.7 and 15.2RC1. Users are advised to upgrade. There are no known workarounds for for this vulnerability.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2023-11-07T19:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/EditAction.java, xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/EditActionTest.java
Commit date: 2023-03-16 16:53:19 +0100
Commit message: XWIKI-20386: CSRF privilege escalation/RCE via the edit action

Diff: diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/EditAction.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/EditAction.java
index 09e61111bf4b6..c70fc7c6d43d3 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/EditAction.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/EditAction.java
@@ -27,10 +27,11 @@
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.xwiki.component.annotation.Component;
+import org.xwiki.csrf.CSRFToken;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.user.UserReference;
 import org.xwiki.user.UserReferenceResolver;
 
@@ -56,10 +57,13 @@ public class EditAction extends XWikiAction
 
     @Inject
     @Named("document")
     private UserReferenceResolver<DocumentReference> documentReferenceUserReferenceResolver;
 
+    @Inject
+    private CSRFToken csrf;
+
     /**
      * Default constructor.
      */
     public EditAction()
     {
@@ -114,23 +118,33 @@ protected XWikiDocument prepareEditedDocument(XWikiContext context) throws XWiki
 
         // The default values from the template can be overwritten by additional request parameters.
         updateDocumentTitleAndContentFromRequest(editedDocument, context);
         editedDocument.readAddedUpdatedAndRemovedObjectsFromForm(editForm, context);
 
-        // If the metadata is modified, modify the effective metadata author
-        if (editedDocument.isMetaDataDirty()) {
-            UserReference userReference =
-                this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
-            editedDocument.getAuthors().setEffectiveMetadataAuthor(userReference);
-            editedDocument.getAuthors().setOriginalMetadataAuthor(userReference);
-        }
+        // Check if the document in modified
+        if (editedDocument.isMetaDataDirty() || editedDocument.isContentDirty()) {
+            // If the document is modified make sure a valid CSRF is provided
+            String token = context.getRequest().getParameter("form_token");
+            if (!this.csrf.isTokenValid(token)) {
+                // or make the document restricted
+                editedDocument.setRestricted(true);
+            }
 
-        // If the content is modified, modify the content author
-        if (editedDocument.isContentDirty()) {
-            UserReference userReference =
-                this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
-            editedDocument.getAuthors().setContentAuthor(userReference);
+            // If the metadata is modified, modify the effective metadata author
+            if (editedDocument.isMetaDataDirty()) {
+                UserReference userReference =
+                    this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
+                editedDocument.getAuthors().setEffectiveMetadataAuthor(userReference);
+                editedDocument.getAuthors().setOriginalMetadataAuthor(userReference);
+            }
+
+            // If the content is modified, modify the content author
+            if (editedDocument.isContentDirty()) {
+                UserReference userReference =
+                    this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
+                editedDocument.getAuthors().setContentAuthor(userReference);
+            }
         }
 
         // Set the current user as creator, author and contentAuthor when the edited document is newly created to avoid
         // using XWikiGuest instead (because those fields were not previously initialized).
         if (editedDocument.isNew()) {
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/EditActionTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/EditActionTest.java
index 6781260196df4..a4ceeee56b412 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/EditActionTest.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/EditActionTest.java
@@ -17,15 +17,18 @@
  * 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.web;
 
+import java.util.Map;
+
 import javax.inject.Named;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
+import org.xwiki.csrf.CSRFToken;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.store.TemporaryAttachmentSessionsManager;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.user.UserReference;
@@ -37,10 +40,11 @@
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 import com.xpn.xwiki.test.reference.ReferenceComponentList;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 /**
@@ -73,20 +77,26 @@
 
     @MockComponent
     @Named("document")
     private UserReferenceSerializer<DocumentReference> documentReferenceUserReferenceSerializer;
 
+    @MockComponent
+    private CSRFToken csrf;
+
     @Mock
     private XWikiRequest request;
 
     @BeforeEach
     public void beforeEach()
     {
         when(this.documentReferenceUserReferenceResolver.resolve(USER_DOCUMENT_REFERENCE)).thenReturn(USER_REFERENCE);
         when(this.documentReferenceUserReferenceSerializer.serialize(USER_REFERENCE)).thenReturn(USER_DOCUMENT_REFERENCE);
 
         this.oldcore.getXWikiContext().setUserReference(USER_DOCUMENT_REFERENCE);
+
+        this.oldcore.getXWikiContext().setRequest(new XWikiServletRequestStub.Builder().
+            setRequestParameters(Map.of("form_token", new String[] {"tokenvalue"})).build());
     }
 
     private String initAndRenderAction() throws XWikiException
     {
         EditForm form = new EditForm();
@@ -139,11 +149,22 @@ void documentAuthorsWhenDocumentExist() throws XWikiException
         assertSame(OTHERUSER_REFERENCE, document.getAuthors().getEffectiveMetadataAuthor());
         assertSame(OTHERUSER_REFERENCE, document.getAuthors().getOriginalMetadataAuthor());
     }
 
     @Test
-    void documentAuthorsWhenDocumentExistAndContentIsModified() throws XWikiException
+    void documentAuthorsWhenDocumentExistAndContentIsModifiedAndInvalidValidCSRF() throws XWikiException
+    {
+        documentAuthorsWhenDocumentExistAndContentIsModified(false);
+    }
+
+    @Test
+    void documentAuthorsWhenDocumentExistAndContentIsModifiedAndValidCSRF() throws XWikiException
+    {
+        documentAuthorsWhenDocumentExistAndContentIsModified(true);
+    }
+
+    void documentAuthorsWhenDocumentExistAndContentIsModified(boolean validToken) throws XWikiException
     {
         XWikiDocument document = this.oldcore.getSpyXWiki().getDocument(new DocumentReference("wiki", "space", "page"),
             this.oldcore.getXWikiContext());
         document.getAuthors().setCreator(OTHERUSER_REFERENCE);
         document.getAuthors().setContentAuthor(OTHERUSER_REFERENCE);
@@ -156,15 +177,18 @@ void documentAuthorsWhenDocumentExistAndContentIsModified() throws XWikiExceptio
         this.oldcore.getXWikiContext().setDoc(document);
         this.oldcore.getXWikiContext().put("tdoc", document);
 
         when(this.request.getParameter("content")).thenReturn("modified content");
 
+        when(this.csrf.isTokenValid("tokenvalue")).thenReturn(validToken);
+
         initAndRenderAction();
 
         document = this.oldcore.getXWikiContext().getDoc();
 
         assertSame(USER_REFERENCE, document.getAuthors().getContentAuthor());
         assertSame(OTHERUSER_REFERENCE, document.getAuthors().getCreator());
         assertSame(OTHERUSER_REFERENCE, document.getAuthors().getEffectiveMetadataAuthor());
         assertSame(OTHERUSER_REFERENCE, document.getAuthors().getOriginalMetadataAuthor());
+        assertEquals(!validToken, document.isRestricted());
     }
 }

Vulnerable code:         // If the metadata is modified, modify the effective metadata author
        if (editedDocument.isMetaDataDirty()) {
            UserReference userReference =
                this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
            editedDocument.getAuthors().setEffectiveMetadataAuthor(userReference);
            editedDocument.getAuthors().setOriginalMetadataAuthor(userReference);
        }
        // If the content is modified, modify the content author
        if (editedDocument.isContentDirty()) {
            UserReference userReference =
                this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
            editedDocument.getAuthors().setContentAuthor(userReference);
    void documentAuthorsWhenDocumentExistAndContentIsModified() throws XWikiException
Fixed code: import org.xwiki.csrf.CSRFToken;
    @Inject
    private CSRFToken csrf;

        // Check if the document in modified
        if (editedDocument.isMetaDataDirty() || editedDocument.isContentDirty()) {
            // If the document is modified make sure a valid CSRF is provided
            String token = context.getRequest().getParameter("form_token");
            if (!this.csrf.isTokenValid(token)) {
                // or make the document restricted
                editedDocument.setRestricted(true);
            }
            // If the metadata is modified, modify the effective metadata author
            if (editedDocument.isMetaDataDirty()) {
                UserReference userReference =
                    this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
                editedDocument.getAuthors().setEffectiveMetadataAuthor(userReference);
                editedDocument.getAuthors().setOriginalMetadataAuthor(userReference);
            }

            // If the content is modified, modify the content author
            if (editedDocument.isContentDirty()) {
                UserReference userReference =
                    this.documentReferenceUserReferenceResolver.resolve(context.getUserReference());
                editedDocument.getAuthors().setContentAuthor(userReference);
            }
import java.util.Map;

import org.xwiki.csrf.CSRFToken;
import static org.junit.jupiter.api.Assertions.assertEquals;
    @MockComponent
    private CSRFToken csrf;


        this.oldcore.getXWikiContext().setRequest(new XWikiServletRequestStub.Builder().
            setRequestParameters(Map.of("form_token", new String[] {"tokenvalue"})).build());
    void documentAuthorsWhenDocumentExistAndContentIsModifiedAndInvalidValidCSRF() throws XWikiException
    {
        documentAuthorsWhenDocumentExistAndContentIsModified(false);
    }

    @Test
    void documentAuthorsWhenDocumentExistAndContentIsModifiedAndValidCSRF() throws XWikiException
    {
        documentAuthorsWhenDocumentExistAndContentIsModified(true);
    }

    void documentAuthorsWhenDocumentExistAndContentIsModified(boolean validToken) throws XWikiException
        when(this.csrf.isTokenValid("tokenvalue")).thenReturn(validToken);

        assertEquals(!validToken, document.isRestricted());