CVE-2023-37910 (Java, published 2023-10-25T18:17Z)
Description: value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. Starting with the introduction of attachment move support in version 14.0-rc-1 and prior to versions 14.4.8, 14.10.4, and 15.0-rc-1, an attacker with edit access on any document (can be the user profile which is editable by default) can move any attachment of any other document to this attacker-controlled document. This allows the attacker to access and possibly publish any attachment of which the name is known, regardless if the attacker has view or edit rights on the source document of this attachment. Further, the attachment is deleted from the source document. This vulnerability has been patched in XWiki 14.4.8, 14.10.4, and 15.0 RC1. There is no workaround apart from upgrading to a fixed version.
Severity: nan (CVSS v3 base score: 8.1)
Weakness: CWE-862 \u2014 Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/main/java/org/xwiki/attachment/internal/refactoring/job/MoveAttachmentJob.java
xwiki-platform-core/xwiki-platform-attachment/xwiki-platform-attachment-api/src/test/java/org/xwiki/attachment/internal/refactoring/job/MoveAttachmentJobTest.java
Commit message: XWIKI-20334: Add rights check to MoveAttachmentJob


Vulnerable code:
            this.progressManager.startStep(this);
            moveAttachment(source, destination, autoRedirect, wiki);
            this.progressManager.endStep(this);

            this.progressManager.startStep(this);
            this.observationManager.notify(new AttachmentMovedEvent((AttachmentReference) source, destination), this,
                this.request);
            this.progressManager.endStep(this);
    protected ModelBridge modelBridge;
    LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

Fixed code:
import org.xwiki.security.authorization.Right;
            if (checkMoveRights(source, destination)) {
                this.progressManager.startStep(this);
                moveAttachment(source, destination, autoRedirect, wiki);
                this.progressManager.endStep(this);

                this.progressManager.startStep(this);
                this.observationManager.notify(new AttachmentMovedEvent((AttachmentReference) source, destination),
                    this,
                    this.request);
                this.progressManager.endStep(this);
            }
    private boolean checkMoveRights(EntityReference source, EntityReference destination)
    {
        // Check view and edit right on source to ensure that the user doesn't get view right through the move.
        // While edit right implies view right, the view right might be explicitly denied.
        boolean hasSourceRight = hasAccess(Right.VIEW, source) && hasAccess(Right.EDIT, source);
        boolean hasDestinationRight = hasAccess(Right.EDIT, destination);

        if (!hasSourceRight) {
            this.logger.error("You don't have sufficient permissions over the source attachment [{}].", source);
        } else if (!hasDestinationRight) {
            // The destination's document might be the same as the source, therefore, only log the error when there is
            // no error regarding the source.
            this.logger.error("You don't have sufficient permissions over the destination attachment [{}].",
                destination);
        }

        return hasSourceRight && hasDestinationRight;
    }

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
    private static final DocumentReference USER2_REFERENCE = new DocumentReference("xwiki", "XWiki", "User2");

    private ModelBridge modelBridge;
    @MockComponent
    private AuthorizationManager authorizationManager;

    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

        // Grant global view and edit right.
        when(this.authorizationManager.hasAccess(eq(Right.VIEW), eq(AUTHOR_REFERENCE), any(AttachmentReference.class)))
            .thenReturn(true);
        when(this.authorizationManager.hasAccess(eq(Right.EDIT), eq(AUTHOR_REFERENCE), any(AttachmentReference.class)))
            .thenReturn(true);
        this.request.setAuthorReference(AUTHOR_REFERENCE);
        this.request.setUserReference(AUTHOR_REFERENCE);
        this.request.setAuthorReference(AUTHOR_REFERENCE);
        this.request.setAuthorReference(AUTHOR_REFERENCE);

    @ParameterizedTest
    @CsvSource({
        "true, true",
        "false, true",
        "true, false",
        "false, false"
    })
    void failWithoutRights(boolean canView, boolean canEdit) throws Exception
    {
        // Request initialization.
        this.request.setEntityReferences(singletonList(SOURCE_ATTACHMENT_LOCATION));
        this.request.setProperty(MoveAttachmentRequest.DESTINATION, TARGET_ATTACHMENT_LOCATION);
        this.request.setProperty(MoveAttachmentRequest.AUTO_REDIRECT, true);
        this.request.setInteractive(false);
        this.request.setUserReference(USER2_REFERENCE);
        this.request.setAuthorReference(AUTHOR_REFERENCE);

        when(this.authorizationManager.hasAccess(Right.EDIT, USER2_REFERENCE, SOURCE_ATTACHMENT_LOCATION))
            .thenReturn(canEdit);
        when(this.authorizationManager.hasAccess(Right.VIEW, USER2_REFERENCE, SOURCE_ATTACHMENT_LOCATION))
            .thenReturn(canView);
        when(this.authorizationManager.hasAccess(Right.EDIT, USER2_REFERENCE, TARGET_ATTACHMENT_LOCATION))
            .thenReturn(false);

        this.job.process(SOURCE_ATTACHMENT_LOCATION);

        // Verify nothing has been modified.
        verifyNoInteractions(this.sourceDocument, this.targetDocument);
        verifyNoInteractions(this.attachmentsManager);
        verify(this.wiki, never()).saveDocument(any(), any(), any());
        verifyNoInteractions(this.targetAuthors, this.sourceAuthors);

        if (!canEdit || !canView) {
            assertEquals("You don't have sufficient permissions over the source attachment "
                + "[Attachment xwiki:Space.Source@oldName].", this.logCapture.getMessage(0));
        } else {
            assertEquals("You don't have sufficient permissions over the destination attachment "
                + "[Attachment xwiki:Space.Target@newName].", this.logCapture.getMessage(0));
        }
    }