CVE-2023-49280 (Java, published 2023-12-04T23:15Z)
Description: value: XWiki Change Request is an XWiki application allowing to request changes on a wiki without publishing directly the changes. Change request allows to edit any page by default, and the changes are then exported in an XML file that anyone can download. So it's possible for an attacker to obtain password hash of users by performing an edit on the user profiles and then downloading the XML file that has been created. This is also true for any document that might contain password field and that a user can view.
This vulnerability impacts all version of Change Request, but the impact depends on the rights that has been set on the wiki since it requires for the user to have the Change request right (allowed by default) and view rights on the page to target. This issue cannot be easily exploited in an automated way. The patch consists in denying to users the right of editing pages that contains a password field with change request. It means that already existing change request for those pages won't be removed by the patch, administrators needs to take care of it. The patch is provided in Change Request 1.10, administrators should upgrade immediately. It's possible to workaround the vulnerability by denying manually the Change request right on some spaces, such as XWiki space which will include any user profile by default.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-522 \u2014 Insufficiently Protected Credentials
The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.
Repository: https://github.com/xwiki-contrib/application-changerequest
Files changed:
application-changerequest-api/src/main/java/org/xwiki/contrib/changerequest/ChangeRequestRightsManager.java
application-changerequest-default/src/checkstyle/checkstyle-suppressions.xml
application-changerequest-default/src/main/java/org/xwiki/contrib/changerequest/internal/DefaultChangeRequestRightsManager.java
application-changerequest-default/src/main/java/org/xwiki/contrib/changerequest/internal/handlers/AbstractChangeRequestActionHandler.java
application-changerequest-default/src/main/java/org/xwiki/contrib/changerequest/script/ChangeRequestAuthorizationScriptService.java
application-changerequest-default/src/test/java/org/xwiki/contrib/changerequest/internal/DefaultChangeRequestRightsManagerTest.java
application-changerequest-default/src/test/java/org/xwiki/contrib/changerequest/internal/handlers/AddChangesChangeRequestHandlerTest.java
application-changerequest-default/src/test/java/org/xwiki/contrib/changerequest/internal/handlers/CreateChangeRequestHandlerTest.java
application-changerequest-ui/src/main/resources/ChangeRequest/Code/EditPageUIX.xml
application-changerequest-ui/src/main/resources/ChangeRequest/Code/SaveButtonsUIX.xml
Commit message: CRAPP-302: Edit button is not displayed for guest user

  * Change the way to deal with change request edition right


Vulnerable code:
        DocumentReference documentReference = mock(DocumentReference.class);
        UserReference userReference = mock(UserReference.class);
        verify(this.requestParameterConverter).convert(request, response);
        DocumentReference documentReference = mock(DocumentReference.class);
        UserReference userReference = mock(UserReference.class);
#set ($hasCREdit = $services.security.authorization.isRightRegistered($crEditName) &amp;&amp; $services.security.authorization.hasAccess($crEditName, $tdoc))
#set ($hasCREdit = $services.security.authorization.isRightRegistered($crEditName) &amp;&amp; $services.security.authorization.hasAccess($crEditName, $tdoc))

Fixed code:

    /**
     * Check if the given user is allowed to use change request to edit the given document reference.
     * @param userReference the user for whom to perform the check
     * @param documentReference the reference for which to check authorization
     * @return {@code true} if the given user is allowed to perform the edition with change request
     * @throws ChangeRequestException in case of problem when performing the check
     * @since 1.10
     */
    default boolean isEditWithChangeRequestAllowed(UserReference userReference, DocumentReference documentReference)
        throws ChangeRequestException
    {
        return false;
    }

    /**
     * Check if the given user is allowed to use change request to create a document having the given reference for
     * parent.
     * @param userReference the user for whom to perform the check
     * @param parentSpaceReference the parent space where the new document would be created
     * @return {@code true} if the given user is allowed to create a document with change request at the given place
     * @throws ChangeRequestException in case of problem when performing the check
     * @since 1.10
     */
    default boolean isCreateWithChangeRequestAllowed(UserReference userReference,
        DocumentReference parentSpaceReference) throws ChangeRequestException
    {
        return false;
    }
  <suppress checks="CyclomaticComplexity" files="AbstractChangeRequestActionHandler.java"/>
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Provider;
import org.xwiki.contrib.changerequest.rights.ChangeRequestRight;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.PasswordClass;
import com.xpn.xwiki.objects.classes.PropertyClass;
    @Inject
    private Provider<XWikiContext> contextProvider;


    @Override
    public boolean isEditWithChangeRequestAllowed(UserReference userReference, DocumentReference documentReference)
        throws ChangeRequestException
    {
        DocumentReference userDocReference = this.userReferenceConverter.convert(userReference);
        boolean hasCREdit = this.authorizationManager.hasAccess(ChangeRequestRight.getRight(), userDocReference,
            documentReference);
        boolean result = false;
        // if the user doesn't have CR edit right, then it's already solved.
        if (hasCREdit) {
            XWikiContext context = this.contextProvider.get();
            try {
                XWikiDocument document = context.getWiki().getDocument(documentReference, context);
                // if it's a new document, we don't care
                if (document.isNew()) {
                    result = true;
                } else {
                    Map<DocumentReference, List<BaseObject>> objects = document.getXObjects();
                    boolean foundOneObjectWithPassword = false;
                    for (Map.Entry<DocumentReference, List<BaseObject>> objectsEntry : objects.entrySet()) {
                        Optional<BaseObject> baseObjectOpt =
                            objectsEntry.getValue().stream().filter(Objects::nonNull).findFirst();
                        if (baseObjectOpt.isPresent() && this.isObjectContainingPassword(baseObjectOpt.get())) {
                            foundOneObjectWithPassword = true;
                            break;
                        }
                    }
                    if (!foundOneObjectWithPassword) {
                        result = true;
                    }
                }
            } catch (XWikiException e) {
                throw new ChangeRequestException(String.format("Cannot load document [%s] to check if CR right should "
                    + "be granted for user [%s]", documentReference, userDocReference), e);
            }
        }
        return result;
    }

    @Override
    public boolean isCreateWithChangeRequestAllowed(UserReference userReference, DocumentReference parentSpaceReference)
        throws ChangeRequestException
    {
        DocumentReference userDocReference = this.userReferenceConverter.convert(userReference);
        return this.authorizationManager.hasAccess(ChangeRequestRight.getRight(), userDocReference,
            parentSpaceReference);
    }

    private boolean isObjectContainingPassword(BaseObject baseObject)
    {
        XWikiContext context = contextProvider.get();
        Collection<PropertyClass> propertyClassCollection = baseObject.getXClass(context).getFieldList();
        for (PropertyClass propertyClass : propertyClassCollection) {
            if (propertyClass instanceof PasswordClass) {
                return true;
            }
        }
        return false;
    }
        UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
        if (!this.changeRequestRightsManager.isEditWithChangeRequestAllowed(currentUserReference, documentReference)) {
            throw new ChangeRequestException(
                String.format("User [%s] is not allowed to edit the document [%s] through a change request.",
                    currentUserReference, documentReference));
        }

    /**
     * Check if the current user is allowed to use change request to edit the given document reference.
     * @param documentReference the reference for which to check authorization
     * @return {@code true} if current user is allowed to perform the edition with change request
     * @throws ChangeRequestException in case of problem when performing the checks
     * @since 1.10
     */
    public boolean isEditWithChangeRequestAllowed(DocumentReference documentReference) throws ChangeRequestException
    {
        UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
        return this.changeRequestRightsManager.isEditWithChangeRequestAllowed(currentUserReference, documentReference);
    }

    /**
     * Check if the current user is allowed to use change request to create a document having the given reference for
     * parent.
     * @param parentSpaceReference the parent space where the new document would be created
     * @return {@code true} if current user is allowed to create a document with change request at the given place
     * @throws ChangeRequestException in case of problem when performing the checks
     * @since 1.10
     */
    public boolean isCreateWithChangeRequestAllowed(DocumentReference parentSpaceReference)
        throws ChangeRequestException
    {
        UserReference currentUserReference = this.currentUserReferenceResolver.resolve(CurrentUserReference.INSTANCE);
        return this.changeRequestRightsManager.isCreateWithChangeRequestAllowed(currentUserReference,
            parentSpaceReference);
    }
import java.util.LinkedHashMap;
import java.util.Map;
import javax.inject.Provider;
import org.xwiki.contrib.changerequest.rights.ChangeRequestRight;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.classes.BaseClass;
import com.xpn.xwiki.objects.classes.PasswordClass;
import com.xpn.xwiki.objects.classes.PropertyClass;
import static org.mockito.Mockito.times;
    @MockComponent
    private Provider<XWikiContext> contextProvider;


    @Test
    void isCreateWithChangeRequestAllowed() throws ChangeRequestException
    {
        Right crRight = ChangeRequestRight.getRight();
        UserReference userReference = mock(UserReference.class);
        DocumentReference userDocReference = mock(DocumentReference.class);
        DocumentReference parentSpaceReference = mock(DocumentReference.class);

        when(this.userReferenceConverter.convert(userReference)).thenReturn(userDocReference);
        when(this.authorizationManager.hasAccess(crRight, userDocReference, parentSpaceReference)).thenReturn(true);
        assertTrue(this.rightsManager.isCreateWithChangeRequestAllowed(userReference, parentSpaceReference));

        verify(this.authorizationManager).hasAccess(crRight, userDocReference, parentSpaceReference);
    }

    @Test
    void isEditWithChangeRequestAllowed() throws ChangeRequestException, XWikiException
    {
        Right crRight = ChangeRequestRight.getRight();
        UserReference userReference = mock(UserReference.class);
        DocumentReference userDocReference = mock(DocumentReference.class);
        DocumentReference documentReference = mock(DocumentReference.class);
        XWikiDocument document = mock(XWikiDocument.class);
        XWiki wiki = mock(XWiki.class);
        XWikiContext context = mock(XWikiContext.class);

        when(this.userReferenceConverter.convert(userReference)).thenReturn(userDocReference);
        when(this.contextProvider.get()).thenReturn(context);
        when(context.getWiki()).thenReturn(wiki);
        when(wiki.getDocument(documentReference, context)).thenReturn(document);
        when(this.authorizationManager.hasAccess(crRight, userDocReference, documentReference)).thenReturn(false);

        assertFalse(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));

        verify(this.authorizationManager).hasAccess(crRight, userDocReference, documentReference);
        verifyNoInteractions(document);

        when(this.authorizationManager.hasAccess(crRight, userDocReference, documentReference)).thenReturn(true);
        when(document.isNew()).thenReturn(true);
        assertTrue(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));

        verify(this.authorizationManager, times(2)).hasAccess(crRight, userDocReference, documentReference);

        when(document.isNew()).thenReturn(false);

        DocumentReference objReference1 = mock(DocumentReference.class);
        DocumentReference objReference2 = mock(DocumentReference.class);

        BaseObject baseObject1 = mock(BaseObject.class);
        BaseObject baseObject2 = mock(BaseObject.class);
        BaseObject baseObject3 = mock(BaseObject.class);

        List<BaseObject> baseObjectList1 = Arrays.asList(null, baseObject1);
        List<BaseObject> baseObjectList2 = Arrays.asList(null, baseObject2, baseObject3);

        Map<DocumentReference, List<BaseObject>> objectMap = new LinkedHashMap<>();
        objectMap.put(objReference1, baseObjectList1);
        objectMap.put(objReference2, baseObjectList2);

        when(document.getXObjects()).thenReturn(objectMap);
        BaseClass class1 = mock(BaseClass.class);
        List<PropertyClass> propertyClasses = List.of(mock(PropertyClass.class), mock(PropertyClass.class),
            mock(PasswordClass.class));
        when(class1.getFieldList()).thenReturn(propertyClasses);
        when(baseObject1.getXClass(context)).thenReturn(class1);

        assertFalse(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
        verify(baseObject1).getXClass(context);
        verifyNoInteractions(baseObject2);
        verifyNoInteractions(baseObject3);

        when(class1.getFieldList()).thenReturn(List.of(mock(PropertyClass.class)));
        BaseClass class2 = mock(BaseClass.class);
        when(class2.getFieldList()).thenReturn(List.of(mock(PasswordClass.class)));
        when(baseObject2.getXClass(context)).thenReturn(class2);

        assertFalse(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
        verify(baseObject1, times(2)).getXClass(context);
        verify(baseObject2).getXClass(context);
        verifyNoInteractions(baseObject3);

        when(class2.getFieldList()).thenReturn(List.of());
        assertTrue(this.rightsManager.isEditWithChangeRequestAllowed(userReference, documentReference));
        verify(baseObject1, times(3)).getXClass(context);
        verify(baseObject2, times(2)).getXClass(context);
        verifyNoInteractions(baseObject3);
    }
import org.xwiki.contrib.changerequest.ChangeRequestException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.times;
        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
            .thenReturn(true);
        DocumentReference documentReference = mock(DocumentReference.class, "editedDoc");
        UserReference userReference = mock(UserReference.class, "currentUser");
        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
            .thenReturn(false);
        ChangeRequestException changeRequestException =
            assertThrows(ChangeRequestException.class, () -> this.handler.handle(changeRequestReference));
        assertEquals("User [currentUser] is not allowed to edit the document [editedDoc] through a change request.",
            changeRequestException.getMessage());

        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
            .thenReturn(true);
        verify(this.requestParameterConverter, times(2)).convert(request, response);
import org.xwiki.contrib.changerequest.ChangeRequestException;
import org.xwiki.contrib.changerequest.ChangeRequestRightsManager;
import static org.junit.jupiter.api.Assertions.assertThrows;
    @MockComponent
    private ChangeRequestRightsManager changeRequestRightsManager;

        DocumentReference documentReference = mock(DocumentReference.class, "editedDoc");
        UserReference userReference = mock(UserReference.class, "currentUser");
        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
            .thenReturn(false);
        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
            .thenReturn(false);
        ChangeRequestException changeRequestException =
            assertThrows(ChangeRequestException.class, () -> this.handler.handle(null));
        assertEquals("User [currentUser] is not allowed to edit the document [editedDoc] through a change request.",
            changeRequestException.getMessage());

        when(this.changeRequestRightsManager.isEditWithChangeRequestAllowed(userReference, documentReference))
            .thenReturn(true);

#set ($hasCREdit = $services.changerequest.authorization.isEditWithChangeRequestAllowed($doc.getDocumentReference()))
#set ($hasCRCreate = $services.changerequest.authorization.isCreateWithChangeRequestAllowed($doc.getDocumentReference()))
#end
#if (!$isReadOnly &amp;&amp; $hasCRCreate)
#set ($hasCREdit = $services.changerequest.authorization.isEditWithChangeRequestAllowed($doc.getDocumentReference()))