CVE-2023-35151 (published 2023-06-23T17:15Z)

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.

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: Manuel Leduc
author_date: 2023-02-21 11:31:19+01:00
msg: XWIKI-16138: Improved REST properties cleanup
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: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -180,6 +180,11 @@
       <artifactId>xwiki-platform-icon-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-mail-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
 
     <!-- Test dependencies -->
     <dependency>

programming_language: HTML
filename: ModelFactory.java
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Set;
 import java.util.Vector;
 import java.util.stream.Collectors;
@@ -35,6 +36,8 @@
 import javax.inject.Inject;
 import javax.inject.Provider;
 import javax.inject.Singleton;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.commons.lang3.StringUtils;
@@ -46,6 +49,8 @@
 import org.xwiki.logging.LogLevel;
 import org.xwiki.logging.event.LogEvent;
 import org.xwiki.logging.tail.LogTail;
+import org.xwiki.mail.EmailAddressObfuscator;
+import org.xwiki.mail.GeneralMailConfiguration;
 import org.xwiki.model.EntityType;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
@@ -129,6 +134,8 @@
 import com.xpn.xwiki.objects.classes.ComputedFieldClass;
 import com.xpn.xwiki.objects.classes.ListClass;
 
+import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
+
 /**
  * Various common tools for resources.
  * 
@@ -167,6 +174,12 @@
     @Inject
     private UserReferenceSerializer<String> userReferenceSerializer;
 
+    @Inject
+    private GeneralMailConfiguration generalMailConfiguration;
+
+    @Inject
+    private EmailAddressObfuscator emailAddressObfuscator;
+
     public ModelFactory()
     {
         this.objectFactory = new ObjectFactory();
@@ -262,7 +275,16 @@ private void fillObjectSummary(ObjectSummary objectSummary, Document doc, BaseOb
         String[] propertyNames = xwikiObject.getPropertyNames();
         if (propertyNames.length > 0) {
             try {
-                objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(propertyNames[0])));
+                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)));
+                }
             } catch (XWikiException e) {
                 // Should never happen
             }
@@ -567,7 +589,7 @@ private Locale getDefaultLocale(Document document)
                 return xcontext.getWiki().getDocument(document.getDocumentReference(), xcontext).getRealLocale();
             } catch (XWikiException e) {
                 this.logger.warn("Failed to get the default locale from [{}]. Root cause is [{}].",
-                    document.getDocumentReference(), ExceptionUtils.getRootCauseMessage(e));
+                    document.getDocumentReference(), getRootCauseMessage(e));
                 // Fall-back on the default locale specified on the translation page, which may not be accurate.
                 return document.getDefaultLocale();
             }
@@ -992,7 +1014,7 @@ public Hierarchy toRestHierarchy(EntityReference targetEntityReference, Boolean
                 } catch (Exception e) {
                     this.logger.warn(
                         "Failed to get the pretty name of entity [{}]. Continue using the entity name. Root cause is [{}].",
-                        entityReference, ExceptionUtils.getRootCauseMessage(e));
+                        entityReference, getRootCauseMessage(e));
                 }
             }
             hierarchy.withItems(hierarchyItem);
@@ -1051,7 +1073,7 @@ private String serializePropertyValue(PropertyInterface property,
                 context.setDoc(document);
             }
         } else {
-            return serializePropertyValue(property);
+            return cleanupBeforeMakingPublic(propertyClass.getClassType(), property);
         }
     }
 
@@ -1246,4 +1268,30 @@ private boolean hasAccess(Property restProperty)
 
         return true;
     }
+
+    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;
+    }
 }

programming_language: Java
filename: ModelFactoryTest.java
change_type: ModificationType.MODIFY
diff: @@ -29,16 +29,24 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.Vector;
+import java.util.stream.Stream;
+
+import javax.mail.internet.InternetAddress;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.Mock;
-import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.mail.EmailAddressObfuscator;
+import org.xwiki.mail.GeneralMailConfiguration;
 import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.model.reference.ObjectReference;
 import org.xwiki.model.reference.SpaceReference;
 import org.xwiki.model.reference.SpaceReferenceResolver;
 import org.xwiki.model.reference.WikiReference;
@@ -54,7 +62,6 @@
 import org.xwiki.security.authorization.Right;
 import org.xwiki.test.LogLevel;
 import org.xwiki.test.junit5.LogCaptureExtension;
-import org.xwiki.test.junit5.mockito.InjectComponentManager;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.wiki.descriptor.WikiDescriptor;
@@ -65,17 +72,22 @@
 import com.xpn.xwiki.XWikiException;
 import com.xpn.xwiki.api.Document;
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseCollection;
 import com.xpn.xwiki.objects.BaseObject;
 import com.xpn.xwiki.objects.StringProperty;
 import com.xpn.xwiki.objects.classes.BaseClass;
+import com.xpn.xwiki.objects.classes.EmailClass;
 import com.xpn.xwiki.objects.classes.PasswordClass;
 import com.xpn.xwiki.objects.classes.StringClass;
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 
+import ch.qos.logback.classic.Level;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -93,9 +105,6 @@
     @RegisterExtension
     private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
 
-    @InjectComponentManager
-    private ComponentManager componentManager;
-
     @MockComponent
     private ContextualAuthorizationManager authorizationManager;
 
@@ -108,6 +117,12 @@
     @MockComponent
     private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
 
+    @MockComponent
+    private GeneralMailConfiguration generalMailConfiguration;
+
+    @MockComponent
+    private EmailAddressObfuscator emailAddressObfuscator;
+
     @InjectMockComponents
     private ModelFactory modelFactory;
 
@@ -206,10 +221,101 @@ void toRestObjectCheckWhichObjectValuesAreAvailableForAdmins() throws Exception
 
         Map<String, String> expectedValues = new HashMap<>();
         expectedValues.put(TEST_STRING_FIELD, TEST_STRING_VALUE);
-        expectedValues.put(TEST_PASSWORD_FIELD, TEST_PASSWORD_VALUE);
+        expectedValues.put(TEST_PASSWORD_FIELD, null);
         assertExpectedPropertyValues(result.getProperties(), expectedValues);
     }
 
+    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"));
+    }
+
     private void assertExpectedPropertyValues(List<Property> properties, Map<String, String> expectedValues)
     {
         Set<String> propertiesFound = new HashSet<>();

programming_language: Java