CVE-2023-29208 (published 2023-04-15T16:15Z)

XWiki Commons are technical libraries common to several other top level XWiki projects. Rights added to a document are not taken into account for viewing it once it's deleted. Note that this vulnerability only impact deleted documents that where containing view rights: the view rights provided on a space of a deleted document are properly checked. The problem has been patched in XWiki 14.10 by checking the rights of current user: only admin and deleter of the document are allowed to view it.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-668
cwe_name: Exposure of Resource to Wrong Sphere
description: The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
extended_description: 
            Resources such as files and directories may be inadvertently exposed through mechanisms such as insecure permissions, or when a program accidentally operates on the wrong object. For example, a program may intend that private files can only be provided to a specific user. This effectively defines a control sphere that is intended to prevent attackers from accessing these private files. If the file permissions are insecure, then parties other than the user will be able to access those files.
            A separate control sphere might effectively require that the user can only access the private files, but not any other files on the system. If the program does not ensure that the user is only requesting private files, then the user might be able to access other files on the system.
            In either case, the end result is that a resource has been exposed to the wrong party.
         
         

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: 2022-10-24 09:31:27+02:00
msg: XWIKI-16285: Error when accessing deleted document
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: DeletedDocument.java
change_type: ModificationType.MODIFY
diff: @@ -22,18 +22,23 @@
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
-import java.util.Objects;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.xwiki.component.util.DefaultParameterizedType;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.security.authorization.Right;
+import org.xwiki.stability.Unstable;
+import org.xwiki.user.UserReference;
+import org.xwiki.user.UserReferenceResolver;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
 import com.xpn.xwiki.doc.XWikiDeletedDocument;
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.store.XWikiRecycleBinStoreInterface;
 import com.xpn.xwiki.util.Programming;
+import com.xpn.xwiki.web.Utils;
 
 /**
  * Information about a deleted document in the recycle bin.
@@ -52,6 +57,8 @@ public class DeletedDocument extends Api
      */
     private final XWikiDeletedDocument deletedDoc;
 
+    private UserReferenceResolver<DocumentReference> userReferenceResolver;
+
     /**
      * Simple constructor, initializes a new API object with the current {@link com.xpn.xwiki.XWikiContext context} and
      * the specified protected {@link com.xpn.xwiki.doc.XWikiDeletedDocument deleted document} object.
@@ -134,6 +141,22 @@ public String getBatchId()
         return this.deletedDoc.getBatchId();
     }
 
+    private UserReferenceResolver<DocumentReference> getUserReferenceResolver()
+    {
+        if (this.userReferenceResolver == null) {
+            this.userReferenceResolver = Utils.getComponent(
+                new DefaultParameterizedType(null, UserReferenceResolver.class, DocumentReference.class), "document");
+        }
+        return this.userReferenceResolver;
+    }
+
+    private boolean hasAccess(Right right)
+    {
+        UserReference userReference = getUserReferenceResolver().resolve(this.context.getUserReference());
+        XWikiRecycleBinStoreInterface recycleBinStore = this.context.getWiki().getRecycleBinStore();
+        return recycleBinStore.hasAccess(right, userReference, this.deletedDoc);
+    }
+
     /**
      * Check if the current user has the right to restore the document.
      *
@@ -141,15 +164,23 @@ public String getBatchId()
      */
     public boolean canUndelete()
     {
-        try {
-            return hasAccessLevel(ADMIN_RIGHT, getFullName()) || hasAccessLevel("undelete", getFullName())
-                || (Objects.equals(this.context.getUserReference(), getDeleterReference())
-                    && hasAccess(Right.EDIT, getDocumentReference()));
-        } catch (XWikiException ex) {
-            // Public APIs should not throw exceptions
-            LOGGER.warn("Exception while checking if entry [{}] can be restored from the recycle bin", getId(), ex);
-            return false;
-        }
+        return hasAccess(Right.EDIT);
+    }
+
+    /**
+     * Check if the current user has the right to view the deleted document.
+     * This is allowed either if the user has admin right on the original reference of the doc, or if they were the
+     * original user who deleted it.
+     *
+     * @return {code true} if the current user is allowed to view the deleted document.
+     * @since 14.10RC1
+     * @since 14.4.7
+     * @since 13.10.11
+     */
+    @Unstable
+    public boolean canView()
+    {
+        return hasAccess(Right.VIEW);
     }
 
     /**

programming_language: Java
filename: DocumentRevisionProvider.java
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,11 @@
 
 import org.xwiki.component.annotation.Role;
 import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AccessDeniedException;
+import org.xwiki.security.authorization.AuthorizationException;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.stability.Unstable;
+import org.xwiki.user.UserReference;
 
 import com.xpn.xwiki.XWikiException;
 
@@ -56,4 +61,25 @@
      * @throws XWikiException when failing to load the document revision
      */
     XWikiDocument getRevision(XWikiDocument document, String revision) throws XWikiException;
+
+    /**
+     * Check if access is granted on the given document revision, for the given user and right: if the access is not
+     * granted this method will throw an {@link AccessDeniedException}.
+     * This method allows each revision provider to have their own check depending on the type of revision.
+     *
+     * @param right the right for which to check if access is granted
+     * @param userReference the user for whom to check access
+     * @param documentReference the reference of the document
+     * @param revision the revision of the document
+     * @throws AuthorizationException if the access is denied
+     * @throws XWikiException in case of problem when loading the revision
+     * @since 14.10RC1
+     * @since 14.4.7
+     * @since 13.10.11
+     */
+    @Unstable
+    default void checkAccess(Right right, UserReference userReference, DocumentReference documentReference,
+        String revision) throws AuthorizationException, XWikiException
+    {
+    }
 }

programming_language: Java
filename: XWikiDeletedDocument.java
change_type: ModificationType.MODIFY
diff: @@ -24,6 +24,7 @@
 import java.util.Locale;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.xwiki.localization.LocaleUtils;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.DocumentReferenceResolver;
@@ -394,4 +395,14 @@ public String getBatchId()
     {
         return batchId;
     }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this)
+            .append("id", id)
+            .append("fullName", fullName)
+            .append("locale", locale)
+            .toString();
+    }
 }

programming_language: Java
filename: DefaultDocumentRevisionProvider.java
change_type: ModificationType.MODIFY
diff: @@ -24,10 +24,14 @@
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
+import org.apache.commons.lang3.tuple.Pair;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
 import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorizationException;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.user.UserReference;
 
 import com.xpn.xwiki.XWikiException;
 import com.xpn.xwiki.doc.DocumentRevisionProvider;
@@ -54,10 +58,8 @@ public class DefaultDocumentRevisionProvider extends AbstractDocumentRevisionPro
     @Named("database")
     private DocumentRevisionProvider databaseDocumentRevisionProvider;
 
-    @Override
-    public XWikiDocument getRevision(DocumentReference reference, String revision) throws XWikiException
+    private Pair<String, String> parseRevision(String revision)
     {
-        // Parse the version
         String revisionPrefix = null;
         if (revision != null) {
             int revisionPrefixIndex = revision.indexOf(':');
@@ -71,7 +73,11 @@ public XWikiDocument getRevision(DocumentReference reference, String revision) t
         } else {
             shortRevision = revision;
         }
+        return Pair.of(revisionPrefix, shortRevision);
+    }
 
+    private DocumentRevisionProvider getProvider(String revisionPrefix) throws XWikiException
+    {
         // Find the provider
         DocumentRevisionProvider provider = this.databaseDocumentRevisionProvider;
         if (revisionPrefix != null) {
@@ -80,12 +86,30 @@ public XWikiDocument getRevision(DocumentReference reference, String revision) t
                 try {
                     provider = componentManager.getInstance(DocumentRevisionProvider.class, revisionPrefix);
                 } catch (ComponentLookupException e) {
-                    throw new XWikiException("Failed to get revision provider for revision [" + revision + "]", e);
+                    throw new XWikiException("Failed to get revision provider for revision [" + revisionPrefix + "]",
+                        e);
                 }
             }
         }
+        return provider;
+    }
+
+    @Override
+    public XWikiDocument getRevision(DocumentReference reference, String revision) throws XWikiException
+    {
+        Pair<String, String> parsedRevision = parseRevision(revision);
 
         // Load the document revision
-        return provider.getRevision(reference, shortRevision);
+        return getProvider(parsedRevision.getLeft()).getRevision(reference, parsedRevision.getRight());
+    }
+
+    @Override
+    public void checkAccess(Right right, UserReference userReference, DocumentReference documentReference,
+        String revision) throws AuthorizationException, XWikiException
+    {
+        Pair<String, String> parsedRevision = parseRevision(revision);
+
+        getProvider(parsedRevision.getLeft())
+            .checkAccess(right, userReference, documentReference, parsedRevision.getRight());
     }
 }

programming_language: Java
filename: DeletedDocumentRevisionProvider.java
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,9 @@
 
 import org.xwiki.component.annotation.Component;
 import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorizationException;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.user.UserReference;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
@@ -61,7 +64,6 @@ public XWikiDocument getRevision(DocumentReference reference, String revision) t
         }
 
         return null;
-
     }
 
     @Override
@@ -69,4 +71,16 @@ public XWikiDocument getRevision(XWikiDocument document, String revision) throws
     {
         return getRevision(document != null ? document.getDocumentReferenceWithLocale() : null, revision);
     }
+
+    @Override
+    public void checkAccess(Right right, UserReference userReference, DocumentReference documentReference,
+        String revision) throws AuthorizationException, XWikiException
+    {
+        XWikiContext xcontext = this.xcontextProvider.get();
+
+        XWikiDeletedDocument deletedDocument = xcontext.getWiki().getDeletedDocument(Long.valueOf(revision), xcontext);
+        if (deletedDocument != null) {
+            xcontext.getWiki().getRecycleBinStore().checkAccess(right, userReference, deletedDocument);
+        }
+    }
 }

programming_language: Java
filename: XWikiHibernateRecycleBinStore.java
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -39,6 +40,12 @@
 import org.xwiki.component.annotation.Component;
 import org.xwiki.component.manager.ComponentLookupException;
 import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorizationException;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.user.UserReference;
+import org.xwiki.user.UserReferenceSerializer;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
@@ -58,6 +65,13 @@
 @Singleton
 public class XWikiHibernateRecycleBinStore extends XWikiHibernateBaseStore implements XWikiRecycleBinStoreInterface
 {
+    @Inject
+    private AuthorizationManager authorizationManager;
+
+    @Inject
+    @Named("document")
+    private UserReferenceSerializer<DocumentReference> userReferenceSerializer;
+
     /**
      * {@link HibernateCallback} used to retrieve from the recycle bin store the deleted versions of a document.
      */
@@ -430,4 +444,31 @@ public void deleteFromRecycleBin(final long index, XWikiContext context, boolean
             return null;
         });
     }
+
+    @Override
+    public void checkAccess(Right right, UserReference userReference, XWikiDeletedDocument deletedDocument)
+        throws AuthorizationException
+    {
+        if (!this.hasAccess(right, userReference, deletedDocument)) {
+            throw new AuthorizationException(
+                String.format("[%s] cannot access deleted document [%s] for right [%s]: "
+                    + "only admin or deleter of the document are authorized",
+                    userReference, deletedDocument, right));
+        }
+    }
+
+    @Override
+    public boolean hasAccess(Right right, UserReference userReference, XWikiDeletedDocument deletedDocument)
+    {
+        DocumentReference documentReference = deletedDocument.getDocumentReference();
+        DocumentReference userDocReference = this.userReferenceSerializer.serialize(userReference);
+
+        boolean result = false;
+        if (this.authorizationManager.hasAccess(Right.ADMIN, userDocReference, documentReference)
+            || (Objects.equals(deletedDocument.getDeleterReference(), userDocReference)
+            && this.authorizationManager.hasAccess(right, userDocReference, documentReference))) {
+            result = true;
+        }
+        return result;
+    }
 }

programming_language: Java
filename: XWikiRecycleBinStoreInterface.java
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,10 @@
 import java.util.Date;
 
 import org.xwiki.component.annotation.Role;
+import org.xwiki.security.authorization.AuthorizationException;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.stability.Unstable;
+import org.xwiki.user.UserReference;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
@@ -232,4 +236,39 @@ default void deleteFromRecycleBin(long index, XWikiContext context, boolean bTra
         // unpredictable.
         deleteFromRecycleBin(new XWikiDocument(), index, context, bTransaction);
     }
+
+    /**
+     * Check if the given deleted document can be accessed for the given right by the given user.
+     * This method only throw the {@link AuthorizationException} if the right is not granted.
+     *
+     * @param right the right to check access for
+     * @param userReference the user for whom to check access
+     * @param deletedDocument the document to be accessed
+     * @throws AuthorizationException if the user doesn't have appropriate right
+     * @since 14.10RC1
+     * @since 14.4.7
+     * @since 13.10.11
+     */
+    @Unstable
+    default void checkAccess(Right right, UserReference userReference, XWikiDeletedDocument deletedDocument) throws
+        AuthorizationException
+    {
+    }
+
+    /**
+     * Check if the given deleted document can be accessed for the given right by the given user.
+     *
+     * @param right the right to check access for
+     * @param userReference the user for whom to check access
+     * @param deletedDocument the document to be accessed
+     * @return {@code true} if the user have appropriate right
+     * @since 14.10RC1
+     * @since 14.4.7
+     * @since 13.10.11
+     */
+    @Unstable
+    default boolean hasAccess(Right right, UserReference userReference, XWikiDeletedDocument deletedDocument)
+    {
+        return false;
+    }
 }

programming_language: PHP
filename: XWikiAction.java
change_type: ModificationType.MODIFY
diff: @@ -82,16 +82,20 @@
 import org.xwiki.resource.entity.EntityResourceReference;
 import org.xwiki.resource.internal.DefaultResourceReferenceHandlerChain;
 import org.xwiki.script.ScriptContextManager;
+import org.xwiki.security.authorization.AuthorizationException;
 import org.xwiki.security.authorization.ContextualAuthorizationManager;
 import org.xwiki.security.authorization.Right;
 import org.xwiki.stability.Unstable;
 import org.xwiki.template.TemplateManager;
+import org.xwiki.user.UserReference;
+import org.xwiki.user.UserReferenceResolver;
 import org.xwiki.velocity.VelocityManager;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.xpn.xwiki.XWiki;
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
+import com.xpn.xwiki.doc.DocumentRevisionProvider;
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.internal.web.LegacyAction;
 import com.xpn.xwiki.monitor.api.MonitorPlugin;
@@ -185,6 +189,13 @@ public abstract class XWikiAction implements LegacyAction
 
     private EntityReferenceSerializer<String> localSerializer;
 
+    @Inject
+    private DocumentRevisionProvider documentRevisionProvider;
+
+    @Inject
+    @Named("document")
+    private UserReferenceResolver<DocumentReference> userReferenceResolver;
+
     /**
      * @return the class of the XWikiForm in charge of parsing the request
      * @since 13.0
@@ -892,6 +903,11 @@ protected boolean supportRedirections()
         return false;
     }
 
+    private UserReference getCurrentUserReference(XWikiContext context)
+    {
+        return this.userReferenceResolver.resolve(context.getUserReference());
+    }
+
     protected void handleRevision(XWikiContext context) throws XWikiException
     {
         String rev = context.getRequest().getParameter("rev");
@@ -906,11 +922,26 @@ protected void handleRevision(XWikiContext context) throws XWikiException
                 Locale locale = LocaleUtils.toLocale(context.getRequest().getParameter("language"), Locale.ROOT);
                 tdoc = new XWikiDocument(tdoc.getDocumentReference(), locale);
             }
-            XWikiDocument rdoc =
-                (!doc.getLocale().equals(tdoc.getLocale())) ? doc : context.getWiki().getDocument(doc, rev, context);
 
-            XWikiDocument rtdoc =
-                (doc.getLocale().equals(tdoc.getLocale())) ? rdoc : context.getWiki().getDocument(tdoc, rev, context);
+            DocumentReference documentReference = doc.getDocumentReference();
+            try {
+                documentRevisionProvider
+                    .checkAccess(Right.VIEW, getCurrentUserReference(context), documentReference, rev);
+            } catch (AuthorizationException e) {
+                Object[] args = { documentReference, rev, context.getUserReference() };
+                throw new XWikiException(XWikiException.MODULE_XWIKI_ACCESS, XWikiException.ERROR_XWIKI_ACCESS_DENIED,
+                    "Access to document {0} with revision {1} has been denied to user {2}", e, args);
+            }
+
+            XWikiDocument rdoc;
+            XWikiDocument rtdoc;
+            if (doc.getLocale().equals(tdoc.getLocale())) {
+                rdoc = this.documentRevisionProvider.getRevision(doc.getDocumentReferenceWithLocale(), rev);
+                rtdoc = rdoc;
+            } else {
+                rdoc = doc;
+                rtdoc = this.documentRevisionProvider.getRevision(tdoc.getDocumentReferenceWithLocale(), rev);
+            }
 
             context.put("tdoc", rtdoc);
             context.put("cdoc", rdoc);

programming_language: Java
filename: UndeleteActionTest.java
change_type: ModificationType.MODIFY
diff: @@ -43,6 +43,7 @@
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.doc.XWikiDeletedDocument;
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.store.XWikiHibernateRecycleBinStore;
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
@@ -104,6 +105,9 @@
     @Mock
     private XWikiDeletedDocument deletedDocument;
 
+    @Mock
+    private XWikiHibernateRecycleBinStore recycleBinStore;
+
     /**
      * The object being tested.
      */
@@ -113,7 +117,7 @@
     void beforeEach() throws Exception
     {
         this.oldcore.getXWikiContext().setRequest(this.request);
-
+        this.oldcore.getSpyXWiki().setRecycleBinStore(this.recycleBinStore);
         XWikiDocument contextDocument = mock(XWikiDocument.class);
         when(contextDocument.getDocumentReference()).thenReturn(DELETED_REFERENCE);
         this.oldcore.getXWikiContext().setDoc(contextDocument);
@@ -143,8 +147,8 @@ void restoreSingleDocument() throws Exception
     {
         when(this.csrfToken.isTokenValid(null)).thenReturn(true);
 
-        when(this.oldcore.getMockRightService().hasAccessLevel(any(), any(), any(), any())).thenReturn(true);
-
+        when(this.recycleBinStore.hasAccess(any(), any(), any()))
+            .thenReturn(true);
         assertFalse(this.undeleteAction.action(this.oldcore.getXWikiContext()));
 
         verify(this.requestFactory).createRestoreRequest(Arrays.asList(ID));
@@ -157,7 +161,7 @@ void restoreSingleDocumentWhenDeleter() throws Exception
     {
         when(this.csrfToken.isTokenValid(null)).thenReturn(true);
 
-        when(this.oldcore.getMockAuthorizationManager().hasAccess(Right.EDIT, null, DELETED_REFERENCE))
+        when(this.recycleBinStore.hasAccess(any(), any(), any()))
             .thenReturn(true);
 
         assertFalse(this.undeleteAction.action(this.oldcore.getXWikiContext()));
@@ -205,8 +209,8 @@ void showBatch() throws Exception
     {
         when(this.request.getParameter("showBatch")).thenReturn("true");
 
-        when(this.oldcore.getMockRightService().hasAccessLevel(any(), any(), any(), any())).thenReturn(true);
-
+        when(this.recycleBinStore.hasAccess(any(), any(), any()))
+            .thenReturn(true);
         assertTrue(this.undeleteAction.action(this.oldcore.getXWikiContext()));
         // Render the "restore" template.
         assertEquals("restore", undeleteAction.render(this.oldcore.getXWikiContext()));
@@ -236,7 +240,8 @@ void restoreBatch() throws Exception
         // Confirmation button pressed.
         when(this.request.getParameter("confirm")).thenReturn("true");
 
-        when(this.oldcore.getMockRightService().hasAccessLevel(any(), any(), any(), any())).thenReturn(true);
+        when(this.recycleBinStore.hasAccess(any(), any(), any()))
+            .thenReturn(true);
 
         assertFalse(this.undeleteAction.action(this.oldcore.getXWikiContext()));
 

programming_language: Java
filename: recyclebinlist.vm
change_type: ModificationType.MODIFY
diff: @@ -43,6 +43,7 @@
   #if($list && $list.size() > 0)
     #set ($canDelete = $list[0].canDelete())
     #set ($canRestore = $list[0].canUndelete())
+    #set ($canView = $list[0].canView())
     <hr />
     <div class="centered $!className">
       <p class="recyclebin-message">$escapetool.xml($services.localization.render($message))</p>
@@ -62,9 +63,13 @@
         <tr>
          <td>$xwiki.getUserName($dd.getDeleter())</td>
          <td>
+         #if ($canView)
            <a class="link-view" href="$doc.getURL('view', $escapetool.url({'rev' : "deleted:${dd.getId()}"}))">
              $xwiki.formatDate($dd.getDate())
            </a>
+         #else
+           $xwiki.formatDate($dd.getDate())
+         #end
          </td>
          #if ($canRestore)
            <td><a href="$xwiki.getURL($dd.fullName, 'undelete', "id=${dd.id}&amp;showBatch=true")">$!{dd.batchId}</a></td>

programming_language: Shell