CVE-2023-35151 (Java, published 2023-06-23T17:15Z)
Description: value: XWiki Platform is a generic wiki platform. Starting in version 7.3-milestone-1 and prior to versions 14.4.8, 14.10.6, and 15.1, ny user can call a REST endpoint and obtain the obfuscated passwords, even when the mail obfuscation is activated. The issue has been patched in XWiki 14.4.8, 14.10.6, and 15.1. There is no known workaround.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-668 \u2014 Exposure of Resource to Wrong Sphere
The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java
Commit message: XWIKI-16138: Improved REST properties cleanup


Vulnerable code:
                objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(propertyNames[0])));
                    document.getDocumentReference(), ExceptionUtils.getRootCauseMessage(e));
                        entityReference, ExceptionUtils.getRootCauseMessage(e));
            return serializePropertyValue(property);
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.test.junit5.mockito.InjectComponentManager;
    @InjectComponentManager
    private ComponentManager componentManager;

        expectedValues.put(TEST_PASSWORD_FIELD, TEST_PASSWORD_VALUE);

Fixed code:
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-mail-api</artifactId>
      <version>${project.version}</version>
    </dependency>
import java.util.Objects;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.xwiki.mail.EmailAddressObfuscator;
import org.xwiki.mail.GeneralMailConfiguration;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;

    @Inject
    private GeneralMailConfiguration generalMailConfiguration;

    @Inject
    private EmailAddressObfuscator emailAddressObfuscator;

                String firstPropertyName = propertyNames[0];
                BaseClass baseClass = xwikiObject.getXClass(this.xcontextProvider.get());
                PropertyInterface field = baseClass.getField(firstPropertyName);
                // The property might not exist in the class. But if it does, it will be a PropertyClass. 
                if (field != null) {
                    String classType = ((com.xpn.xwiki.objects.classes.PropertyClass) field).getClassType();
                    objectSummary.setHeadline(cleanupBeforeMakingPublic(classType, xwikiObject.get(firstPropertyName)));
                } else {
                    objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(firstPropertyName)));
                }
                    document.getDocumentReference(), getRootCauseMessage(e));
                        entityReference, getRootCauseMessage(e));
            return cleanupBeforeMakingPublic(propertyClass.getClassType(), property);

    private String cleanupBeforeMakingPublic(String type, PropertyInterface baseProperty)
    {
        String cleanedUpStringValue;
        if (Objects.equals(type, PASSWORD_TYPE)) {
            cleanedUpStringValue = null;
        } else {
            cleanedUpStringValue = serializePropertyValue(baseProperty);
            // We obfuscate the email only if the obfuscation has been activated, and if the current user does not have 
            // the right to edit the document containing the base property.
            // A user allowed to edit a document has to view the unescaped email to be able to edit it correctly. 
            if (Objects.equals(type, "Email") && this.generalMailConfiguration.shouldObfuscate()
                && !this.authorizationManagerProvider.get().hasAccess(Right.EDIT, baseProperty.getReference()))
            {
                try {
                    cleanedUpStringValue =
                        this.emailAddressObfuscator.obfuscate(InternetAddress.parse(cleanedUpStringValue)[0]);
                } catch (AddressException e) {
                    this.logger.warn("Failed to parse [{}] to an email address. Cause: [{}]", cleanedUpStringValue,
                        getRootCauseMessage(e));
                    cleanedUpStringValue = "";
                }
            }
        }
        return cleanedUpStringValue;
    }
import java.util.stream.Stream;

import javax.mail.internet.InternetAddress;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.xwiki.mail.EmailAddressObfuscator;
import org.xwiki.mail.GeneralMailConfiguration;
import org.xwiki.model.reference.ObjectReference;
import com.xpn.xwiki.objects.BaseCollection;
import com.xpn.xwiki.objects.classes.EmailClass;
import ch.qos.logback.classic.Level;

import static org.mockito.ArgumentMatchers.any;
    @MockComponent
    private GeneralMailConfiguration generalMailConfiguration;

    @MockComponent
    private EmailAddressObfuscator emailAddressObfuscator;

        expectedValues.put(TEST_PASSWORD_FIELD, null);
    public static Stream<Arguments> toRestObjectWithObfuscatedMailSource()
    {
        return Stream.of(
            // no mail obfuscation
            Arguments.of(false, true, "user@domain.tld", "user@domain.tld", null),
            // mail obfuscation activated but the current user has edit rights
            Arguments.of(true, true, "user@domain.tld", "user@domain.tld", null),
            // mail obfuscation activated and the current user does not have edit rights 
            Arguments.of(true, false, "user@domain.tld", "u...@domain.tld", null),
            // mail obfuscation activated, the current user does not have edit rights and the mail is badly formatted
            Arguments.of(true, false, "wrong@", "",
                "Failed to parse [wrong@] to an email address. Cause: [AddressException: Missing domain]")
        );
    }

    @ParameterizedTest
    @MethodSource("toRestObjectWithObfuscatedMailSource")
    void toRestObjectWithObfuscatedMail(boolean shouldObfuscate, boolean hasEditRight, String inputMail,
        String expectedEmail, String expectedWarning) throws Exception
    {
        ObjectReference objectReference = mock(ObjectReference.class);
        when(this.generalMailConfiguration.shouldObfuscate()).thenReturn(shouldObfuscate);
        when(this.authorizationManager.hasAccess(Right.EDIT, objectReference)).thenReturn(hasEditRight);
        when(this.emailAddressObfuscator.obfuscate(any(InternetAddress.class))).thenReturn(expectedEmail);

        BaseObject xwikiObject = mock(BaseObject.class);
        BaseClass xwikiClass = mock(BaseClass.class);

        when(xwikiObject.getPropertyNames()).thenReturn(new String[] {});
        when(xwikiObject.getXClass(this.xcontext)).thenReturn(xwikiClass);
        when(xwikiObject.getClassName()).thenReturn("Some.XClass");
        when(xwikiObject.getNumber()).thenReturn(0);

        EmailClass emailField = new EmailClass();
        emailField.setName("emailValue");
        StringProperty emailElement = new StringProperty();
        emailElement.setName("emailValue");
        emailElement.setClassType("Password");
        emailElement.setValue(inputMail);
        BaseCollection baseCollection = mock(BaseCollection.class);
        when(baseCollection.getReference()).thenReturn(objectReference);
        emailElement.setObject(baseCollection);
        when(xwikiObject.get("emailValue")).thenReturn(emailElement);

        when(xwikiClass.getProperties()).thenReturn(new java.lang.Object[] { emailField });

        Object result = this.modelFactory.toRestObject(this.baseURI, this.testDocument, xwikiObject, false, false);

        assertExpectedPropertyValues(result.getProperties(), Map.of("emailValue", expectedEmail));

        if (expectedWarning != null) {
            assertEquals(expectedWarning, this.logCapture.getMessage(0));
            assertEquals(Level.WARN, this.logCapture.getLogEvent(0).getLevel());
        }
    }

    @Test
    void toRestObjectWithObfuscatedMailInHeader() throws Exception
    {
        ObjectReference objectReference = mock(ObjectReference.class);
        when(this.generalMailConfiguration.shouldObfuscate()).thenReturn(true);
        when(this.authorizationManager.hasAccess(Right.EDIT, objectReference)).thenReturn(false);
        when(this.emailAddressObfuscator.obfuscate(any(InternetAddress.class))).thenReturn("u...@domain.tld");

        BaseObject xwikiObject = mock(BaseObject.class);
        BaseClass xwikiClass = mock(BaseClass.class);

        when(xwikiObject.getPropertyNames()).thenReturn(new String[] {});
        when(xwikiObject.getXClass(this.xcontext)).thenReturn(xwikiClass);
        when(xwikiObject.getClassName()).thenReturn("Some.XClass");
        when(xwikiObject.getNumber()).thenReturn(0);

        EmailClass emailField = new EmailClass();
        emailField.setName("emailValue");
        StringProperty emailElement = new StringProperty();
        emailElement.setName("emailValue");
        emailElement.setClassType("Password");
        emailElement.setValue("user@domain.tld");
        BaseCollection baseCollection = mock(BaseCollection.class);
        when(baseCollection.getReference()).thenReturn(objectReference);
        emailElement.setObject(baseCollection);
        when(xwikiObject.get("emailValue")).thenReturn(emailElement);

        when(xwikiClass.getProperties()).thenReturn(new java.lang.Object[] { emailField });
        when(xwikiObject.getPropertyNames()).thenReturn(new String[] { "emailValue" });

        Object result = this.modelFactory.toRestObject(this.baseURI, this.testDocument, xwikiObject, false, false);

        assertExpectedPropertyValues(result.getProperties(), Map.of("emailValue", "u...@domain.tld"));
    }