High severityNVD Advisory· Published Jun 23, 2023· Updated Nov 27, 2024
XWiki Platform may show email addresses in clear in REST results
CVE-2023-35151
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-rest-serverMaven | >= 7.3-milestone-1, < 14.4.8 | 14.4.8 |
org.xwiki.platform:xwiki-platform-rest-serverMaven | >= 14.5, < 14.10.6 | 14.10.6 |
org.xwiki.platform:xwiki-platform-rest-serverMaven | >= 15.0-rc-1, < 15.1 | 15.1 |
Affected products
1- Range: >= 7.3-milestone-1, < 14.4.8
Patches
1824cd742ecf5XWIKI-16138: Improved REST properties cleanup
3 files changed · +169 −10
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml+5 −0 modified@@ -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>
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java+52 −4 modified@@ -28,13 +28,16 @@ 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; 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 @@ public class ModelFactory @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; + } }
xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java+112 −6 modified@@ -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 @@ class ModelFactoryTest @RegisterExtension private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN); - @InjectComponentManager - private ComponentManager componentManager; - @MockComponent private ContextualAuthorizationManager authorizationManager; @@ -108,6 +117,12 @@ class ModelFactoryTest @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<>();
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-8g9c-c9cm-9c56ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-35151ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/824cd742ecf5439971247da11bfe7e0ad2b10edeghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-8g9c-c9cm-9c56ghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-16138ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.