CVE-2023-29208: Exposure of Resource to Wrong Sphere (CWE-668)
value: 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.
CWE description: The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
CVSS v3 base score: 7.5
Published: 2023-04-15T16:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/DeletedDocument.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DocumentRevisionProvider.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDeletedDocument.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DefaultDocumentRevisionProvider.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DeletedDocumentRevisionProvider.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateRecycleBinStore.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiRecycleBinStoreInterface.java, xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/XWikiAction.java, xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/UndeleteActionTest.java, xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/recyclebinlist.vm
Commit date: 2022-11-02 15:34:38 +0100
Commit message: XWIKI-16285: Error when accessing deleted document

Diff: diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/DeletedDocument.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/DeletedDocument.java
index da4443c8c6baf..2582514256f6c 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/DeletedDocument.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/api/DeletedDocument.java
@@ -20,22 +20,27 @@
 package com.xpn.xwiki.api;
 
 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.
  *
  * @version $Id$
@@ -50,10 +55,12 @@ public class DeletedDocument extends Api
     /**
      * The internal object wrapped by this 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.
      *
      * @param deletedDoc the internal object wrapped by this API
@@ -132,26 +139,50 @@ public long getId()
     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.
      *
      * @return {@code true} if the current user can restore this document, {@code false} otherwise
      */
     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);
     }
 
     /**
      * @return {@code true} if the current user can permanently delete this document, {@code false} otherwise
      * @xwiki.xwikicfg xwiki.store.recyclebin.adminWaitDays How many days should an administrator wait before being able
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DocumentRevisionProvider.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DocumentRevisionProvider.java
index 77d57cd41337e..d418c282eca8f 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DocumentRevisionProvider.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/DocumentRevisionProvider.java
@@ -19,10 +19,15 @@
  */
 package com.xpn.xwiki.doc;
 
 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;
 
 /**
  * Provide generic way of requesting a specific revision of a document from several sources (database, installed XAR
@@ -54,6 +59,27 @@
      * @param revision the revision of the document
      * @return the {@link XWikiDocument} instance or null if none existing
      * @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
+    {
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDeletedDocument.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDeletedDocument.java
index c0c145414f6da..cfc107dd95ca1 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDeletedDocument.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/doc/XWikiDeletedDocument.java
@@ -22,10 +22,11 @@
 import java.io.IOException;
 import java.util.Date;
 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;
 
 import com.xpn.xwiki.XWikiContext;
@@ -392,6 +393,16 @@ protected void setBatchId(String batchId)
      */
     public String getBatchId()
     {
         return batchId;
     }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this)
+            .append("id", id)
+            .append("fullName", fullName)
+            .append("locale", locale)
+            .toString();
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DefaultDocumentRevisionProvider.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DefaultDocumentRevisionProvider.java
index 484c28047c19e..a14cb81430c45 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DefaultDocumentRevisionProvider.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DefaultDocumentRevisionProvider.java
@@ -22,14 +22,18 @@
 import javax.inject.Inject;
 import javax.inject.Named;
 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;
 import com.xpn.xwiki.doc.XWikiDocument;
 
@@ -52,14 +56,12 @@ public class DefaultDocumentRevisionProvider extends AbstractDocumentRevisionPro
 
     @Inject
     @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(':');
             if (revisionPrefixIndex > 0) {
                 revisionPrefix = revision.substring(0, revisionPrefixIndex);
@@ -69,23 +71,45 @@ public XWikiDocument getRevision(DocumentReference reference, String revision) t
         if (revisionPrefix != null) {
             shortRevision = revision.substring(revisionPrefix.length() + 1);
         } 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) {
             ComponentManager componentManager = this.componentManagerProvider.get();
             if (componentManager.hasComponent(DocumentRevisionProvider.class, revisionPrefix)) {
                 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());
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DeletedDocumentRevisionProvider.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DeletedDocumentRevisionProvider.java
index df995f4fe9d96..54a761ed6ec4c 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DeletedDocumentRevisionProvider.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/internal/doc/DeletedDocumentRevisionProvider.java
@@ -24,10 +24,13 @@
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
 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;
 import com.xpn.xwiki.doc.DocumentRevisionProvider;
 import com.xpn.xwiki.doc.XWikiDeletedDocument;
@@ -59,14 +62,25 @@ public XWikiDocument getRevision(DocumentReference reference, String revision) t
             && (reference == null || deletedDocument.getDocumentReference().equals(reference))) {
             return deletedDocument.restoreDocument(xcontext);
         }
 
         return null;
-
     }
 
     @Override
     public XWikiDocument getRevision(XWikiDocument document, String revision) throws XWikiException
     {
         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);
+        }
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateRecycleBinStore.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateRecycleBinStore.java
index 9ca1bfd2ccd75..14afb5b8175cf 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateRecycleBinStore.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiHibernateRecycleBinStore.java
@@ -20,10 +20,11 @@
 package com.xpn.xwiki.store;
 
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 import javax.persistence.criteria.CriteriaBuilder;
@@ -37,10 +38,16 @@
 import org.hibernate.Session;
 import org.slf4j.Logger;
 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;
 import com.xpn.xwiki.doc.XWikiDeletedDocument;
 import com.xpn.xwiki.doc.XWikiDeletedDocumentContent;
@@ -56,10 +63,17 @@
 @Component
 @Named(XWikiHibernateBaseStore.HINT)
 @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.
      */
     private static class DeletedDocumentsHibernateCallback implements HibernateCallback<XWikiDeletedDocument[]>
     {
@@ -428,6 +442,33 @@ public void deleteFromRecycleBin(final long index, XWikiContext context, boolean
             deleteDeletedDocumentContent(deletedDocument, bTransaction);
 
             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;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiRecycleBinStoreInterface.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiRecycleBinStoreInterface.java
index eae53a1cff08d..f69e42df91459 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiRecycleBinStoreInterface.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/store/XWikiRecycleBinStoreInterface.java
@@ -20,10 +20,14 @@
 package com.xpn.xwiki.store;
 
 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;
 import com.xpn.xwiki.doc.XWikiDeletedDocument;
 import com.xpn.xwiki.doc.XWikiDocument;
@@ -230,6 +234,41 @@ default void deleteFromRecycleBin(long index, XWikiContext context, boolean bTra
         // XXX: Depending on how an older implementation handled the XWikiDocument argument, it's relatively safer to
         // pass an empty document than null. However, if the document's reference is actually used, the result might be
         // 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;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/XWikiAction.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/XWikiAction.java
index 55c4397c6e207..1ade7dac19182 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/XWikiAction.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/web/XWikiAction.java
@@ -80,20 +80,24 @@
 import org.xwiki.resource.ResourceReferenceManager;
 import org.xwiki.resource.ResourceType;
 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;
 import com.xpn.xwiki.objects.BaseObject;
 import com.xpn.xwiki.plugin.fileupload.FileUploadPlugin;
@@ -183,10 +187,17 @@ public abstract class XWikiAction implements LegacyAction
 
     private EntityNameValidationConfiguration entityNameValidationConfiguration;
 
     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
      */
     @Unstable
@@ -890,10 +901,15 @@ public String render(XWikiContext context) throws XWikiException
     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");
         if (rev != null) {
             context.put("rev", rev);
@@ -904,15 +920,30 @@ protected void handleRevision(XWikiContext context) throws XWikiException
             if (rev.startsWith("deleted") && !StringUtils.isEmpty(context.getRequest().getParameter("language"))
                 && doc == tdoc) {
                 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);
             context.put("doc", rdoc);
         }
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/UndeleteActionTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/UndeleteActionTest.java
index 5a0af8bab66c8..baed31e75a221 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/UndeleteActionTest.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/web/UndeleteActionTest.java
@@ -41,10 +41,11 @@
 import org.xwiki.test.mockito.MockitoComponentManager;
 
 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;
 import com.xpn.xwiki.test.reference.ReferenceComponentList;
 
@@ -102,20 +103,23 @@
     private RestoreRequest jobRequest;
 
     @Mock
     private XWikiDeletedDocument deletedDocument;
 
+    @Mock
+    private XWikiHibernateRecycleBinStore recycleBinStore;
+
     /**
      * The object being tested.
      */
     private UndeleteAction undeleteAction = new UndeleteAction();
 
     @BeforeEach
     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);
 
         when(this.jobExecutor.execute(anyString(), any())).thenReturn(this.job);
@@ -141,12 +145,12 @@ void beforeEach() throws Exception
     @Test
     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));
         verify(jobExecutor).execute(RefactoringJobs.RESTORE, jobRequest);
         verify(job).join();
@@ -155,11 +159,11 @@ void restoreSingleDocument() throws Exception
     @Test
     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()));
 
         verify(this.requestFactory).createRestoreRequest(Arrays.asList(ID));
@@ -203,12 +207,12 @@ void recycleBinDisabledOrInvalidId() throws Exception
     @Test
     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()));
 
         // Just make sure that we stop to the display, since the "confirm=true" parameter was not passed.
@@ -234,11 +238,12 @@ void restoreBatch() throws Exception
         when(this.request.getParameter("includeBatch")).thenReturn("true");
 
         // 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()));
 
         verify(this.requestFactory).createRestoreRequest(batchId);
         verify(jobExecutor).execute(RefactoringJobs.RESTORE, jobRequest);
diff --git a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/recyclebinlist.vm b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/recyclebinlist.vm
index 44899b06eb454..288cc03c6ff3d 100644
--- a/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/recyclebinlist.vm
+++ b/xwiki-platform-core/xwiki-platform-web/xwiki-platform-web-templates/src/main/resources/templates/recyclebinlist.vm
@@ -41,10 +41,11 @@
     #set ($message = 'core.recyclebin.showlistmsg')
   #end  
   #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>
       <table class="centered">
        <thead>
@@ -60,13 +61,17 @@
        <tbody>
        #foreach($dd in $list)
         <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>
          #end
          <td>

Vulnerable code: import java.util.Objects;
        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;
        }
    @Override
    public XWikiDocument getRevision(DocumentReference reference, String revision) throws XWikiException
        // Parse the version
                    throw new XWikiException("Failed to get revision provider for revision [" + revision + "]", e);
        return provider.getRevision(reference, shortRevision);

            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);

        when(this.oldcore.getMockRightService().hasAccessLevel(any(), any(), any(), any())).thenReturn(true);

        when(this.oldcore.getMockAuthorizationManager().hasAccess(Right.EDIT, null, DELETED_REFERENCE))
        when(this.oldcore.getMockRightService().hasAccessLevel(any(), any(), any(), any())).thenReturn(true);

        when(this.oldcore.getMockRightService().hasAccessLevel(any(), any(), any(), any())).thenReturn(true);
Fixed code: import org.xwiki.component.util.DefaultParameterizedType;
import org.xwiki.stability.Unstable;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceResolver;
import com.xpn.xwiki.store.XWikiRecycleBinStoreInterface;
import com.xpn.xwiki.web.Utils;
    private UserReferenceResolver<DocumentReference> userReferenceResolver;

    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);
    }

        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);
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;

    /**
     * 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
    {
    }
import org.apache.commons.lang3.builder.ToStringBuilder;

    @Override
    public String toString()
    {
        return new ToStringBuilder(this)
            .append("id", id)
            .append("fullName", fullName)
            .append("locale", locale)
            .toString();
    }
import org.apache.commons.lang3.tuple.Pair;
import org.xwiki.security.authorization.AuthorizationException;
import org.xwiki.security.authorization.Right;
import org.xwiki.user.UserReference;
    private Pair<String, String> parseRevision(String revision)
        return Pair.of(revisionPrefix, shortRevision);
    }
    private DocumentRevisionProvider getProvider(String revisionPrefix) throws XWikiException
    {
                    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);
        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());
import org.xwiki.security.authorization.AuthorizationException;
import org.xwiki.security.authorization.Right;
import org.xwiki.user.UserReference;

    @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);
        }
    }
import java.util.Objects;
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;
    @Inject
    private AuthorizationManager authorizationManager;

    @Inject
    @Named("document")
    private UserReferenceSerializer<DocumentReference> userReferenceSerializer;


    @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;
    }
import org.xwiki.security.authorization.AuthorizationException;
import org.xwiki.security.authorization.Right;
import org.xwiki.stability.Unstable;
import org.xwiki.user.UserReference;

    /**
     * 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;
    }
import org.xwiki.security.authorization.AuthorizationException;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceResolver;
import com.xpn.xwiki.doc.DocumentRevisionProvider;
    @Inject
    private DocumentRevisionProvider documentRevisionProvider;

    @Inject
    @Named("document")
    private UserReferenceResolver<DocumentReference> userReferenceResolver;

    private UserReference getCurrentUserReference(XWikiContext context)
    {
        return this.userReferenceResolver.resolve(context.getUserReference());
    }

            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);
            }
import com.xpn.xwiki.store.XWikiHibernateRecycleBinStore;
    @Mock
    private XWikiHibernateRecycleBinStore recycleBinStore;

        this.oldcore.getSpyXWiki().setRecycleBinStore(this.recycleBinStore);
        when(this.recycleBinStore.hasAccess(any(), any(), any()))
            .thenReturn(true);
        when(this.recycleBinStore.hasAccess(any(), any(), any()))
        when(this.recycleBinStore.hasAccess(any(), any(), any()))
            .thenReturn(true);
        when(this.recycleBinStore.hasAccess(any(), any(), any()))
            .thenReturn(true);
    #set ($canView = $list[0].canView())
         #if ($canView)
         #else
           $xwiki.formatDate($dd.getDate())
         #end