VYPR
Critical severityNVD Advisory· Published Apr 10, 2024· Updated Aug 13, 2024

XWiki Platform: Remote code execution from edit in multilingual wikis via translations

CVE-2024-31983

Description

XWiki Platform is a generic wiki platform. In multilingual wikis, translations can be edited by any user who has edit right, circumventing the rights that are normally required for authoring translations (script right for user-scope translations, wiki admin for translations on the wiki). Starting in version 4.3-milestone-2 and prior to versions 4.10.20, 15.5.4, and 15.10-rc-1, this can be exploited for remote code execution if the translation value is not properly escaped where it is used. This has been patched in XWiki 14.10.20, 15.5.4 and 15.10RC1. As a workaround, one may restrict edit rights on documents that contain translations.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.xwiki.platform:xwiki-platform-localization-source-wikiMaven
>= 4.3-milestone-2, < 14.10.2014.10.20
org.xwiki.platform:xwiki-platform-localization-source-wikiMaven
>= 15.0-rc-1, < 15.5.415.5.4
org.xwiki.platform:xwiki-platform-localization-source-wikiMaven
>= 15.6-rc-1, < 15.10-rc-115.10-rc-1

Affected products

1

Patches

3
73aef9648bbf

XWIKI-21411: Improve check of translation document author rights

https://github.com/xwiki/xwiki-platformpjeanjeanOct 24, 2023via ghsa
4 files changed · +310 14
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/AbstractDocumentTranslationBundle.java+26 8 modified
    @@ -138,39 +138,57 @@ protected void setReference(DocumentReference reference)
             setId(this.idPrefix + this.serializer.serialize(reference));
         }
     
    -    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
    +    /**
    +     * Gets the document that defines the translation bundle for a given locale.
    +     *
    +     * @param locale the requested locale
    +     * @return the document defining the translation bundle, or null if it could not be fetched yet and requires a retry
    +     */
    +    protected XWikiDocument getDocumentLocaleBundle(Locale locale) throws Exception
         {
             XWikiContext context = this.contextProvider.get();
     
             if (context == null) {
    -            // No context for some reason, lets try later
    +            // No context for some reason, let's try later.
                 return null;
             }
     
             XWiki xwiki = context.getWiki();
     
             if (xwiki == null) {
    -            // No XWiki instance ready, lets try later
    +            // No XWiki instance ready, let's try later.
                 return null;
             }
     
             XWikiDocument document = xwiki.getDocument(this.documentReference, context);
     
             if (locale != null && !locale.equals(Locale.ROOT) && !locale.equals(document.getDefaultLocale())) {
                 document = xwiki.getDocument(new DocumentReference(document.getDocumentReference(), locale), context);
    +        }
     
    -            if (document.isNew()) {
    -                // No document found for this locale
    -                return LocalizedTranslationBundle.EMPTY;
    -            }
    +        return document;
    +    }
    +
    +    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
    +    {
    +        XWikiDocument document = getDocumentLocaleBundle(locale);
    +
    +        if (document == null) {
    +            // Either no context or XWiki instance not ready, let's try later.
    +            return null;
    +        }
    +
    +        if (document.isNew()) {
    +            // No document found for this locale.
    +            return LocalizedTranslationBundle.EMPTY;
             }
     
             String content = document.getContent();
     
             Properties properties = new Properties();
             properties.load(new StringReader(content));
     
    -        // Convert to LocalBundle
    +        // Convert to LocalBundle.
             DefaultLocalizedTranslationBundle localeBundle = new DefaultLocalizedTranslationBundle(this, locale);
     
             TranslationMessageParser parser = getTranslationMessageParser();
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/ComponentDocumentTranslationBundle.java+47 1 modified
    @@ -19,6 +19,8 @@
      */
     package org.xwiki.localization.wiki.internal;
     
    +import java.util.Locale;
    +
     import org.xwiki.bridge.event.WikiDeletedEvent;
     import org.xwiki.component.descriptor.ComponentDescriptor;
     import org.xwiki.component.manager.ComponentLookupException;
    @@ -27,6 +29,11 @@
     import org.xwiki.localization.message.TranslationMessageParser;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.observation.event.Event;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +
    +import com.xpn.xwiki.XWiki;
    +import com.xpn.xwiki.XWikiContext;
    +import com.xpn.xwiki.doc.XWikiDocument;
     
     /**
      * Component wiki document based implementation of Bundle.
    @@ -37,6 +44,8 @@
      */
     public class ComponentDocumentTranslationBundle extends AbstractDocumentTranslationBundle
     {
    +    private DocumentTranslationBundleFactory factory;
    +
         private ComponentDescriptor<TranslationBundle> descriptor;
     
         /**
    @@ -45,17 +54,54 @@ public class ComponentDocumentTranslationBundle extends AbstractDocumentTranslat
          * @param componentManager used to lookup components needed to manipulate wiki documents
          * @param translationMessageParser the parser to use for each message
          * @param descriptor the component descriptor used to unregister the bundle
    +     * @param factory the factory
          * @throws ComponentLookupException failed to lookup some required components
          */
         public ComponentDocumentTranslationBundle(String idPrefix, DocumentReference documentReference,
             ComponentManager componentManager, TranslationMessageParser translationMessageParser,
    -        ComponentDescriptor<TranslationBundle> descriptor) throws ComponentLookupException
    +        ComponentDescriptor<TranslationBundle> descriptor, DocumentTranslationBundleFactory factory)
    +        throws ComponentLookupException
         {
             super(idPrefix, documentReference, componentManager, translationMessageParser);
     
    +        this.factory = factory;
             this.descriptor = descriptor;
         }
     
    +    /**
    +     * {@inheritDoc}
    +     * This overrides the default implementation to first check the author rights for the document.
    +     *
    +     * @param locale the requested locale
    +     * @return the document defining the translation bundle if its author has the necessary rights, the default locale
    +     *     otherwise, and fallback on the original implementation if the document doesn't exist or could not be fetched
    +     */
    +    @Override
    +    protected XWikiDocument getDocumentLocaleBundle(Locale locale) throws Exception
    +    {
    +        XWikiDocument document = super.getDocumentLocaleBundle(locale);
    +
    +        if (document != null && !document.isNew()) {
    +            XWikiContext context = this.contextProvider.get();
    +            XWiki xwiki = context.getWiki();
    +            XWikiDocument defaultLocaleDocument = xwiki.getDocument(this.documentReference, context);
    +
    +            if (defaultLocaleDocument != document) {
    +                // We only need to check rights for non-default locales.
    +                try {
    +                    this.factory.checkRegistrationAuthorizationForDocumentLocaleBundle(document, defaultLocaleDocument);
    +                } catch (AccessDeniedException e) {
    +                    this.logger.warn("Failed to load and register the translation for locale [{}] from document [{}]. "
    +                        + "Falling back to default locale.", locale, document.getDocumentReference());
    +                    // We return the default translation bundle if the requested one has permission issues.
    +                    return defaultLocaleDocument;
    +                }
    +            }
    +        }
    +
    +        return document;
    +    }
    +
         @Override
         public void onEvent(Event event, Object source, Object data)
         {
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/DocumentTranslationBundleFactory.java+29 5 modified
    @@ -56,6 +56,7 @@
     import org.xwiki.localization.wiki.internal.TranslationDocumentModel.Scope;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.model.reference.DocumentReferenceResolver;
    +import org.xwiki.model.reference.EntityReference;
     import org.xwiki.model.reference.EntityReferenceSerializer;
     import org.xwiki.model.reference.WikiReference;
     import org.xwiki.observation.EventListener;
    @@ -324,7 +325,7 @@ private ComponentDocumentTranslationBundle createComponentDocumentBundle(XWikiDo
             try {
                 documentBundle =
                     new ComponentDocumentTranslationBundle(ID_PREFIX, document.getDocumentReference(),
    -                    this.componentManagerProvider.get(), this.translationParser, descriptor);
    +                    this.componentManagerProvider.get(), this.translationParser, descriptor, this);
             } catch (ComponentLookupException e) {
                 throw new TranslationBundleDoesNotExistsException("Failed to create document bundle", e);
             }
    @@ -434,6 +435,23 @@ private void registerTranslationBundle(XWikiDocument document) throws Translatio
             }
         }
     
    +    /**
    +     * Checks that the author of a document defining a translation bundle has the necessary rights to make it
    +     * available, based on the scope of the default locale translation bundle.
    +     *
    +     * @param document the document defining the translation bundle to check
    +     * @param defaultLocaleDocument the document containing the default locale translation bundle
    +     * @throws AccessDeniedException when the document author does not have enough rights for the defined scope
    +     */
    +    protected void checkRegistrationAuthorizationForDocumentLocaleBundle(XWikiDocument document,
    +        XWikiDocument defaultLocaleDocument) throws AccessDeniedException
    +    {
    +        Scope scope = getScope(defaultLocaleDocument);
    +        if (scope != null && scope != Scope.ON_DEMAND) {
    +            checkRegistrationAuthorization(document, scope);
    +        }
    +    }
    +
         /**
          * @param document the translation document
          * @param scope the scope
    @@ -442,18 +460,24 @@ private void registerTranslationBundle(XWikiDocument document) throws Translatio
          */
         private void checkRegistrationAuthorization(XWikiDocument document, Scope scope) throws AccessDeniedException
         {
    +        EntityReference entityReference;
             switch (scope) {
                 case GLOBAL:
                     this.authorizationManager.checkAccess(Right.PROGRAM, document.getAuthorReference(), null);
    +                this.authorizationManager.checkAccess(Right.PROGRAM, document.getContentAuthorReference(), null);
                     break;
                 case WIKI:
    -                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), document
    -                    .getDocumentReference().getWikiReference());
    +                entityReference = document.getDocumentReference().getWikiReference();
    +                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), entityReference);
    +                this.authorizationManager.checkAccess(Right.ADMIN, document.getContentAuthorReference(),
    +                    entityReference);
                     break;
                 case USER:
                     if (this.configuration.isRestrictUserTranslations()) {
    -                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(),
    -                        document.getDocumentReference());
    +                    entityReference = document.getDocumentReference();
    +                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(), entityReference);
    +                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getContentAuthorReference(),
    +                        entityReference);
                     }
                     break;
                 default:
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/test/java/org/xwiki/localization/wiki/internal/ComponentDocumentTranslationBundleTest.java+208 0 added
    @@ -0,0 +1,208 @@
    +/*
    + * See the NOTICE file distributed with this work for additional
    + * information regarding copyright ownership.
    + *
    + * This is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU Lesser General Public License as
    + * published by the Free Software Foundation; either version 2.1 of
    + * the License, or (at your option) any later version.
    + *
    + * This software is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    + * Lesser General Public License for more details.
    + *
    + * You should have received a copy of the GNU Lesser General Public
    + * License along with this software; if not, write to the Free
    + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
    + * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
    + */
    +package org.xwiki.localization.wiki.internal;
    +
    +import java.util.Collections;
    +import java.util.Locale;
    +
    +import javax.inject.Inject;
    +
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.extension.RegisterExtension;
    +import org.mockito.Mock;
    +import org.xwiki.cache.Cache;
    +import org.xwiki.cache.CacheManager;
    +import org.xwiki.cache.config.CacheConfiguration;
    +import org.xwiki.component.internal.multi.ComponentManagerManager;
    +import org.xwiki.job.event.status.JobProgressManager;
    +import org.xwiki.localization.LocalizationManager;
    +import org.xwiki.localization.Translation;
    +import org.xwiki.localization.TranslationBundleFactory;
    +import org.xwiki.localization.internal.DefaultLocalizationManager;
    +import org.xwiki.localization.internal.DefaultTranslationBundleContext;
    +import org.xwiki.localization.messagetool.internal.MessageToolTranslationMessageParser;
    +import org.xwiki.model.internal.DefaultModelContext;
    +import org.xwiki.model.reference.DocumentReference;
    +import org.xwiki.observation.internal.DefaultObservationManager;
    +import org.xwiki.query.Query;
    +import org.xwiki.query.QueryManager;
    +import org.xwiki.rendering.internal.parser.plain.PlainTextBlockParser;
    +import org.xwiki.rendering.internal.renderer.plain.PlainTextBlockRenderer;
    +import org.xwiki.rendering.internal.renderer.plain.PlainTextRendererFactory;
    +import org.xwiki.rendering.syntax.Syntax;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +import org.xwiki.security.authorization.Right;
    +import org.xwiki.test.LogLevel;
    +import org.xwiki.test.annotation.BeforeComponent;
    +import org.xwiki.test.annotation.ComponentList;
    +import org.xwiki.test.junit5.LogCaptureExtension;
    +import org.xwiki.test.junit5.mockito.MockComponent;
    +
    +import com.xpn.xwiki.doc.XWikiDocument;
    +import com.xpn.xwiki.objects.BaseObject;
    +import com.xpn.xwiki.test.MockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
    +import com.xpn.xwiki.test.reference.ReferenceComponentList;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.Mockito.any;
    +import static org.mockito.Mockito.doThrow;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.times;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.when;
    +
    +/**
    + * Unit tests for {@link ComponentDocumentTranslationBundle}.
    + *
    + * @version $Id$
    + */
    +@OldcoreTest
    +@ComponentList({
    +    DocumentTranslationBundleFactory.class,
    +    DefaultLocalizationManager.class,
    +    DefaultTranslationBundleContext.class,
    +    TranslationDocumentClassInitializer.class,
    +    DefaultModelContext.class,
    +    PlainTextBlockRenderer.class,
    +    PlainTextRendererFactory.class,
    +    DefaultObservationManager.class,
    +    MessageToolTranslationMessageParser.class,
    +    PlainTextBlockParser.class
    +})
    +@ReferenceComponentList
    +class ComponentDocumentTranslationBundleTest
    +{
    +    private static final DocumentReference TRANSLATION_ROOT_REFERENCE = new DocumentReference("xwiki", "space",
    +        "Translations");
    +
    +    private static final DocumentReference ADMIN_USER_REFERENCE = new DocumentReference("xwiki", "XWiki", "XWikiAdmin");
    +
    +    @InjectMockitoOldcore
    +    private MockitoOldcore oldcore;
    +
    +    @MockComponent
    +    private WikiTranslationConfiguration translationConfiguration;
    +
    +    @MockComponent
    +    private QueryManager mockQueryManager;
    +
    +    @Mock
    +    private Query mockQuery;
    +
    +    @MockComponent
    +    private ComponentManagerManager componentManagerManager;
    +
    +    @MockComponent
    +    private JobProgressManager jobProgressManager;
    +
    +    @Inject
    +    private LocalizationManager localization;
    +
    +    private XWikiDocument translationFrDocument;
    +
    +    private DocumentReference adminUserReference;
    +
    +    /**
    +     * Capture logs.
    +     */
    +    @RegisterExtension
    +    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
    +
    +    @MockComponent
    +    private CacheManager cacheManager;
    +
    +    @BeforeComponent
    +    void before() throws Exception
    +    {
    +        Cache<Object> cache = mock(Cache.class);
    +        when(this.cacheManager.createNewCache(any(CacheConfiguration.class))).thenReturn(cache);
    +    }
    +
    +    @BeforeEach
    +    void setUp() throws Exception
    +    {
    +        this.oldcore.notifyDocumentCreatedEvent(true);
    +
    +        when(this.mockQueryManager.createQuery(anyString(), anyString())).thenReturn(this.mockQuery);
    +        when(this.mockQuery.execute()).thenReturn(Collections.emptyList());
    +
    +        when(this.componentManagerManager.getComponentManager("wiki:xwiki", true)).thenReturn(this.oldcore.getMocker());
    +        this.oldcore.getMocker().getInstance(TranslationBundleFactory.class, DocumentTranslationBundleFactory.ID);
    +
    +        XWikiDocument translationRootDocument = this.oldcore.getSpyXWiki().getDocument(TRANSLATION_ROOT_REFERENCE,
    +            this.oldcore.getXWikiContext());
    +
    +        BaseObject translationObject = translationRootDocument.newXObject(
    +            new DocumentReference("xwiki", "XWiki", "TranslationDocumentClass"),
    +            this.oldcore.getXWikiContext());
    +        translationObject.setStringValue(TranslationDocumentModel.TRANSLATIONCLASS_PROP_SCOPE,
    +            TranslationDocumentModel.Scope.WIKI.toString());
    +
    +        translationRootDocument.setSyntax(Syntax.PLAIN_1_0);
    +        translationRootDocument.setContent("xwiki.translation=root");
    +        translationRootDocument.setAuthorReference(ADMIN_USER_REFERENCE);
    +        this.oldcore.getSpyXWiki().saveDocument(translationRootDocument, this.oldcore.getXWikiContext());
    +
    +        this.translationFrDocument = translationRootDocument.getTranslatedDocument(Locale.FRENCH,
    +            this.oldcore.getXWikiContext());
    +        if (this.translationFrDocument == translationRootDocument) {
    +            this.translationFrDocument =
    +                new XWikiDocument(this.translationFrDocument.getDocumentReference(), Locale.FRENCH);
    +            this.translationFrDocument.setDefaultLocale(this.translationFrDocument.getDefaultLocale());
    +        }
    +        this.translationFrDocument.setSyntax(Syntax.PLAIN_1_0);
    +        this.translationFrDocument.setContent("xwiki.translation=fr");
    +        this.oldcore.getSpyXWiki().saveDocument(this.translationFrDocument, this.oldcore.getXWikiContext());
    +
    +        doThrow(new AccessDeniedException(Right.SCRIPT, null, translationRootDocument.getDocumentReference()))
    +            .when(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.ADMIN, null,
    +                TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +
    +    @Test
    +    void checkTranslationWithExpectedRights() throws Exception
    +    {
    +        this.translationFrDocument.setAuthorReference(ADMIN_USER_REFERENCE);
    +        this.translationFrDocument.setContentAuthorReference(ADMIN_USER_REFERENCE);
    +        this.oldcore.getSpyXWiki().saveDocument(this.translationFrDocument, this.oldcore.getXWikiContext());
    +        Translation frTranslation = this.localization.getTranslation("xwiki.translation", Locale.FRENCH);
    +        assertEquals("fr", frTranslation.getRawSource());
    +        // Authorizations are checked twice because the mocked behavior is not actual locale bundle registration.
    +        verify(this.oldcore.getMockAuthorizationManager(), times(4)).checkAccess(Right.ADMIN, ADMIN_USER_REFERENCE,
    +            TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +
    +    @Test
    +    void checkTranslationWithoutExpectedRights() throws Exception
    +    {
    +        Translation frTranslation = this.localization.getTranslation("xwiki.translation", Locale.FRENCH);
    +        assertEquals(
    +            "Failed to load and register the translation for locale [fr] from document [xwiki:space.Translations]. "
    +                + "Falling back to default locale.",
    +            this.logCapture.getMessage(0));
    +        assertEquals("root", frTranslation.getRawSource());
    +        verify(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.ADMIN, null,
    +            TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +}
    
2a9ce88f3366

XWIKI-21411: Improve check of translation document author rights

https://github.com/xwiki/xwiki-platformpjeanjeanOct 24, 2023via ghsa
4 files changed · +310 14
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/AbstractDocumentTranslationBundle.java+26 8 modified
    @@ -138,39 +138,57 @@ protected void setReference(DocumentReference reference)
             setId(this.idPrefix + this.serializer.serialize(reference));
         }
     
    -    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
    +    /**
    +     * Gets the document that defines the translation bundle for a given locale.
    +     *
    +     * @param locale the requested locale
    +     * @return the document defining the translation bundle, or null if it could not be fetched yet and requires a retry
    +     */
    +    protected XWikiDocument getDocumentLocaleBundle(Locale locale) throws Exception
         {
             XWikiContext context = this.contextProvider.get();
     
             if (context == null) {
    -            // No context for some reason, lets try later
    +            // No context for some reason, let's try later.
                 return null;
             }
     
             XWiki xwiki = context.getWiki();
     
             if (xwiki == null) {
    -            // No XWiki instance ready, lets try later
    +            // No XWiki instance ready, let's try later.
                 return null;
             }
     
             XWikiDocument document = xwiki.getDocument(this.documentReference, context);
     
             if (locale != null && !locale.equals(Locale.ROOT) && !locale.equals(document.getDefaultLocale())) {
                 document = xwiki.getDocument(new DocumentReference(document.getDocumentReference(), locale), context);
    +        }
     
    -            if (document.isNew()) {
    -                // No document found for this locale
    -                return LocalizedTranslationBundle.EMPTY;
    -            }
    +        return document;
    +    }
    +
    +    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
    +    {
    +        XWikiDocument document = getDocumentLocaleBundle(locale);
    +
    +        if (document == null) {
    +            // Either no context or XWiki instance not ready, let's try later.
    +            return null;
    +        }
    +
    +        if (document.isNew()) {
    +            // No document found for this locale.
    +            return LocalizedTranslationBundle.EMPTY;
             }
     
             String content = document.getContent();
     
             Properties properties = new Properties();
             properties.load(new StringReader(content));
     
    -        // Convert to LocalBundle
    +        // Convert to LocalBundle.
             DefaultLocalizedTranslationBundle localeBundle = new DefaultLocalizedTranslationBundle(this, locale);
     
             TranslationMessageParser parser = getTranslationMessageParser();
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/ComponentDocumentTranslationBundle.java+47 1 modified
    @@ -19,6 +19,8 @@
      */
     package org.xwiki.localization.wiki.internal;
     
    +import java.util.Locale;
    +
     import org.xwiki.bridge.event.WikiDeletedEvent;
     import org.xwiki.component.descriptor.ComponentDescriptor;
     import org.xwiki.component.manager.ComponentLookupException;
    @@ -27,6 +29,11 @@
     import org.xwiki.localization.message.TranslationMessageParser;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.observation.event.Event;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +
    +import com.xpn.xwiki.XWiki;
    +import com.xpn.xwiki.XWikiContext;
    +import com.xpn.xwiki.doc.XWikiDocument;
     
     /**
      * Component wiki document based implementation of Bundle.
    @@ -37,6 +44,8 @@
      */
     public class ComponentDocumentTranslationBundle extends AbstractDocumentTranslationBundle
     {
    +    private DocumentTranslationBundleFactory factory;
    +
         private ComponentDescriptor<TranslationBundle> descriptor;
     
         /**
    @@ -45,17 +54,54 @@ public class ComponentDocumentTranslationBundle extends AbstractDocumentTranslat
          * @param componentManager used to lookup components needed to manipulate wiki documents
          * @param translationMessageParser the parser to use for each message
          * @param descriptor the component descriptor used to unregister the bundle
    +     * @param factory the factory
          * @throws ComponentLookupException failed to lookup some required components
          */
         public ComponentDocumentTranslationBundle(String idPrefix, DocumentReference documentReference,
             ComponentManager componentManager, TranslationMessageParser translationMessageParser,
    -        ComponentDescriptor<TranslationBundle> descriptor) throws ComponentLookupException
    +        ComponentDescriptor<TranslationBundle> descriptor, DocumentTranslationBundleFactory factory)
    +        throws ComponentLookupException
         {
             super(idPrefix, documentReference, componentManager, translationMessageParser);
     
    +        this.factory = factory;
             this.descriptor = descriptor;
         }
     
    +    /**
    +     * {@inheritDoc}
    +     * This overrides the default implementation to first check the author rights for the document.
    +     *
    +     * @param locale the requested locale
    +     * @return the document defining the translation bundle if its author has the necessary rights, the default locale
    +     *     otherwise, and fallback on the original implementation if the document doesn't exist or could not be fetched
    +     */
    +    @Override
    +    protected XWikiDocument getDocumentLocaleBundle(Locale locale) throws Exception
    +    {
    +        XWikiDocument document = super.getDocumentLocaleBundle(locale);
    +
    +        if (document != null && !document.isNew()) {
    +            XWikiContext context = this.contextProvider.get();
    +            XWiki xwiki = context.getWiki();
    +            XWikiDocument defaultLocaleDocument = xwiki.getDocument(this.documentReference, context);
    +
    +            if (defaultLocaleDocument != document) {
    +                // We only need to check rights for non-default locales.
    +                try {
    +                    this.factory.checkRegistrationAuthorizationForDocumentLocaleBundle(document, defaultLocaleDocument);
    +                } catch (AccessDeniedException e) {
    +                    this.logger.warn("Failed to load and register the translation for locale [{}] from document [{}]. "
    +                        + "Falling back to default locale.", locale, document.getDocumentReference());
    +                    // We return the default translation bundle if the requested one has permission issues.
    +                    return defaultLocaleDocument;
    +                }
    +            }
    +        }
    +
    +        return document;
    +    }
    +
         @Override
         public void onEvent(Event event, Object source, Object data)
         {
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/DocumentTranslationBundleFactory.java+29 5 modified
    @@ -56,6 +56,7 @@
     import org.xwiki.localization.wiki.internal.TranslationDocumentModel.Scope;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.model.reference.DocumentReferenceResolver;
    +import org.xwiki.model.reference.EntityReference;
     import org.xwiki.model.reference.EntityReferenceSerializer;
     import org.xwiki.model.reference.WikiReference;
     import org.xwiki.observation.EventListener;
    @@ -324,7 +325,7 @@ private ComponentDocumentTranslationBundle createComponentDocumentBundle(XWikiDo
             try {
                 documentBundle =
                     new ComponentDocumentTranslationBundle(ID_PREFIX, document.getDocumentReference(),
    -                    this.componentManagerProvider.get(), this.translationParser, descriptor);
    +                    this.componentManagerProvider.get(), this.translationParser, descriptor, this);
             } catch (ComponentLookupException e) {
                 throw new TranslationBundleDoesNotExistsException("Failed to create document bundle", e);
             }
    @@ -434,6 +435,23 @@ private void registerTranslationBundle(XWikiDocument document) throws Translatio
             }
         }
     
    +    /**
    +     * Checks that the author of a document defining a translation bundle has the necessary rights to make it
    +     * available, based on the scope of the default locale translation bundle.
    +     *
    +     * @param document the document defining the translation bundle to check
    +     * @param defaultLocaleDocument the document containing the default locale translation bundle
    +     * @throws AccessDeniedException when the document author does not have enough rights for the defined scope
    +     */
    +    protected void checkRegistrationAuthorizationForDocumentLocaleBundle(XWikiDocument document,
    +        XWikiDocument defaultLocaleDocument) throws AccessDeniedException
    +    {
    +        Scope scope = getScope(defaultLocaleDocument);
    +        if (scope != null && scope != Scope.ON_DEMAND) {
    +            checkRegistrationAuthorization(document, scope);
    +        }
    +    }
    +
         /**
          * @param document the translation document
          * @param scope the scope
    @@ -442,18 +460,24 @@ private void registerTranslationBundle(XWikiDocument document) throws Translatio
          */
         private void checkRegistrationAuthorization(XWikiDocument document, Scope scope) throws AccessDeniedException
         {
    +        EntityReference entityReference;
             switch (scope) {
                 case GLOBAL:
                     this.authorizationManager.checkAccess(Right.PROGRAM, document.getAuthorReference(), null);
    +                this.authorizationManager.checkAccess(Right.PROGRAM, document.getContentAuthorReference(), null);
                     break;
                 case WIKI:
    -                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), document
    -                    .getDocumentReference().getWikiReference());
    +                entityReference = document.getDocumentReference().getWikiReference();
    +                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), entityReference);
    +                this.authorizationManager.checkAccess(Right.ADMIN, document.getContentAuthorReference(),
    +                    entityReference);
                     break;
                 case USER:
                     if (this.configuration.isRestrictUserTranslations()) {
    -                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(),
    -                        document.getDocumentReference());
    +                    entityReference = document.getDocumentReference();
    +                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(), entityReference);
    +                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getContentAuthorReference(),
    +                        entityReference);
                     }
                     break;
                 default:
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/test/java/org/xwiki/localization/wiki/internal/ComponentDocumentTranslationBundleTest.java+208 0 added
    @@ -0,0 +1,208 @@
    +/*
    + * See the NOTICE file distributed with this work for additional
    + * information regarding copyright ownership.
    + *
    + * This is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU Lesser General Public License as
    + * published by the Free Software Foundation; either version 2.1 of
    + * the License, or (at your option) any later version.
    + *
    + * This software is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    + * Lesser General Public License for more details.
    + *
    + * You should have received a copy of the GNU Lesser General Public
    + * License along with this software; if not, write to the Free
    + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
    + * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
    + */
    +package org.xwiki.localization.wiki.internal;
    +
    +import java.util.Collections;
    +import java.util.Locale;
    +
    +import javax.inject.Inject;
    +
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.extension.RegisterExtension;
    +import org.mockito.Mock;
    +import org.xwiki.cache.Cache;
    +import org.xwiki.cache.CacheManager;
    +import org.xwiki.cache.config.CacheConfiguration;
    +import org.xwiki.component.internal.multi.ComponentManagerManager;
    +import org.xwiki.job.event.status.JobProgressManager;
    +import org.xwiki.localization.LocalizationManager;
    +import org.xwiki.localization.Translation;
    +import org.xwiki.localization.TranslationBundleFactory;
    +import org.xwiki.localization.internal.DefaultLocalizationManager;
    +import org.xwiki.localization.internal.DefaultTranslationBundleContext;
    +import org.xwiki.localization.messagetool.internal.MessageToolTranslationMessageParser;
    +import org.xwiki.model.internal.DefaultModelContext;
    +import org.xwiki.model.reference.DocumentReference;
    +import org.xwiki.observation.internal.DefaultObservationManager;
    +import org.xwiki.query.Query;
    +import org.xwiki.query.QueryManager;
    +import org.xwiki.rendering.internal.parser.plain.PlainTextBlockParser;
    +import org.xwiki.rendering.internal.renderer.plain.PlainTextBlockRenderer;
    +import org.xwiki.rendering.internal.renderer.plain.PlainTextRendererFactory;
    +import org.xwiki.rendering.syntax.Syntax;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +import org.xwiki.security.authorization.Right;
    +import org.xwiki.test.LogLevel;
    +import org.xwiki.test.annotation.BeforeComponent;
    +import org.xwiki.test.annotation.ComponentList;
    +import org.xwiki.test.junit5.LogCaptureExtension;
    +import org.xwiki.test.junit5.mockito.MockComponent;
    +
    +import com.xpn.xwiki.doc.XWikiDocument;
    +import com.xpn.xwiki.objects.BaseObject;
    +import com.xpn.xwiki.test.MockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
    +import com.xpn.xwiki.test.reference.ReferenceComponentList;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.Mockito.any;
    +import static org.mockito.Mockito.doThrow;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.times;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.when;
    +
    +/**
    + * Unit tests for {@link ComponentDocumentTranslationBundle}.
    + *
    + * @version $Id$
    + */
    +@OldcoreTest
    +@ComponentList({
    +    DocumentTranslationBundleFactory.class,
    +    DefaultLocalizationManager.class,
    +    DefaultTranslationBundleContext.class,
    +    TranslationDocumentClassInitializer.class,
    +    DefaultModelContext.class,
    +    PlainTextBlockRenderer.class,
    +    PlainTextRendererFactory.class,
    +    DefaultObservationManager.class,
    +    MessageToolTranslationMessageParser.class,
    +    PlainTextBlockParser.class
    +})
    +@ReferenceComponentList
    +class ComponentDocumentTranslationBundleTest
    +{
    +    private static final DocumentReference TRANSLATION_ROOT_REFERENCE = new DocumentReference("xwiki", "space",
    +        "Translations");
    +
    +    private static final DocumentReference ADMIN_USER_REFERENCE = new DocumentReference("xwiki", "XWiki", "XWikiAdmin");
    +
    +    @InjectMockitoOldcore
    +    private MockitoOldcore oldcore;
    +
    +    @MockComponent
    +    private WikiTranslationConfiguration translationConfiguration;
    +
    +    @MockComponent
    +    private QueryManager mockQueryManager;
    +
    +    @Mock
    +    private Query mockQuery;
    +
    +    @MockComponent
    +    private ComponentManagerManager componentManagerManager;
    +
    +    @MockComponent
    +    private JobProgressManager jobProgressManager;
    +
    +    @Inject
    +    private LocalizationManager localization;
    +
    +    private XWikiDocument translationFrDocument;
    +
    +    private DocumentReference adminUserReference;
    +
    +    /**
    +     * Capture logs.
    +     */
    +    @RegisterExtension
    +    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
    +
    +    @MockComponent
    +    private CacheManager cacheManager;
    +
    +    @BeforeComponent
    +    void before() throws Exception
    +    {
    +        Cache<Object> cache = mock(Cache.class);
    +        when(this.cacheManager.createNewCache(any(CacheConfiguration.class))).thenReturn(cache);
    +    }
    +
    +    @BeforeEach
    +    void setUp() throws Exception
    +    {
    +        this.oldcore.notifyDocumentCreatedEvent(true);
    +
    +        when(this.mockQueryManager.createQuery(anyString(), anyString())).thenReturn(this.mockQuery);
    +        when(this.mockQuery.execute()).thenReturn(Collections.emptyList());
    +
    +        when(this.componentManagerManager.getComponentManager("wiki:xwiki", true)).thenReturn(this.oldcore.getMocker());
    +        this.oldcore.getMocker().getInstance(TranslationBundleFactory.class, DocumentTranslationBundleFactory.ID);
    +
    +        XWikiDocument translationRootDocument = this.oldcore.getSpyXWiki().getDocument(TRANSLATION_ROOT_REFERENCE,
    +            this.oldcore.getXWikiContext());
    +
    +        BaseObject translationObject = translationRootDocument.newXObject(
    +            new DocumentReference("xwiki", "XWiki", "TranslationDocumentClass"),
    +            this.oldcore.getXWikiContext());
    +        translationObject.setStringValue(TranslationDocumentModel.TRANSLATIONCLASS_PROP_SCOPE,
    +            TranslationDocumentModel.Scope.WIKI.toString());
    +
    +        translationRootDocument.setSyntax(Syntax.PLAIN_1_0);
    +        translationRootDocument.setContent("xwiki.translation=root");
    +        translationRootDocument.setAuthorReference(ADMIN_USER_REFERENCE);
    +        this.oldcore.getSpyXWiki().saveDocument(translationRootDocument, this.oldcore.getXWikiContext());
    +
    +        this.translationFrDocument = translationRootDocument.getTranslatedDocument(Locale.FRENCH,
    +            this.oldcore.getXWikiContext());
    +        if (this.translationFrDocument == translationRootDocument) {
    +            this.translationFrDocument =
    +                new XWikiDocument(this.translationFrDocument.getDocumentReference(), Locale.FRENCH);
    +            this.translationFrDocument.setDefaultLocale(this.translationFrDocument.getDefaultLocale());
    +        }
    +        this.translationFrDocument.setSyntax(Syntax.PLAIN_1_0);
    +        this.translationFrDocument.setContent("xwiki.translation=fr");
    +        this.oldcore.getSpyXWiki().saveDocument(this.translationFrDocument, this.oldcore.getXWikiContext());
    +
    +        doThrow(new AccessDeniedException(Right.SCRIPT, null, translationRootDocument.getDocumentReference()))
    +            .when(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.ADMIN, null,
    +                TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +
    +    @Test
    +    void checkTranslationWithExpectedRights() throws Exception
    +    {
    +        this.translationFrDocument.setAuthorReference(ADMIN_USER_REFERENCE);
    +        this.translationFrDocument.setContentAuthorReference(ADMIN_USER_REFERENCE);
    +        this.oldcore.getSpyXWiki().saveDocument(this.translationFrDocument, this.oldcore.getXWikiContext());
    +        Translation frTranslation = this.localization.getTranslation("xwiki.translation", Locale.FRENCH);
    +        assertEquals("fr", frTranslation.getRawSource());
    +        // Authorizations are checked twice because the mocked behavior is not actual locale bundle registration.
    +        verify(this.oldcore.getMockAuthorizationManager(), times(4)).checkAccess(Right.ADMIN, ADMIN_USER_REFERENCE,
    +            TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +
    +    @Test
    +    void checkTranslationWithoutExpectedRights() throws Exception
    +    {
    +        Translation frTranslation = this.localization.getTranslation("xwiki.translation", Locale.FRENCH);
    +        assertEquals(
    +            "Failed to load and register the translation for locale [fr] from document [xwiki:space.Translations]. "
    +                + "Falling back to default locale.",
    +            this.logCapture.getMessage(0));
    +        assertEquals("root", frTranslation.getRawSource());
    +        verify(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.ADMIN, null,
    +            TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +}
    
c4c8d61c30de

XWIKI-21411: Improve check of translation document author rights

https://github.com/xwiki/xwiki-platformpjeanjeanOct 24, 2023via ghsa
4 files changed · +310 14
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/AbstractDocumentTranslationBundle.java+26 8 modified
    @@ -138,39 +138,57 @@ protected void setReference(DocumentReference reference)
             setId(this.idPrefix + this.serializer.serialize(reference));
         }
     
    -    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
    +    /**
    +     * Gets the document that defines the translation bundle for a given locale.
    +     *
    +     * @param locale the requested locale
    +     * @return the document defining the translation bundle, or null if it could not be fetched yet and requires a retry
    +     */
    +    protected XWikiDocument getDocumentLocaleBundle(Locale locale) throws Exception
         {
             XWikiContext context = this.contextProvider.get();
     
             if (context == null) {
    -            // No context for some reason, lets try later
    +            // No context for some reason, let's try later.
                 return null;
             }
     
             XWiki xwiki = context.getWiki();
     
             if (xwiki == null) {
    -            // No XWiki instance ready, lets try later
    +            // No XWiki instance ready, let's try later.
                 return null;
             }
     
             XWikiDocument document = xwiki.getDocument(this.documentReference, context);
     
             if (locale != null && !locale.equals(Locale.ROOT) && !locale.equals(document.getDefaultLocale())) {
                 document = xwiki.getDocument(new DocumentReference(document.getDocumentReference(), locale), context);
    +        }
     
    -            if (document.isNew()) {
    -                // No document found for this locale
    -                return LocalizedTranslationBundle.EMPTY;
    -            }
    +        return document;
    +    }
    +
    +    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
    +    {
    +        XWikiDocument document = getDocumentLocaleBundle(locale);
    +
    +        if (document == null) {
    +            // Either no context or XWiki instance not ready, let's try later.
    +            return null;
    +        }
    +
    +        if (document.isNew()) {
    +            // No document found for this locale.
    +            return LocalizedTranslationBundle.EMPTY;
             }
     
             String content = document.getContent();
     
             Properties properties = new Properties();
             properties.load(new StringReader(content));
     
    -        // Convert to LocalBundle
    +        // Convert to LocalBundle.
             DefaultLocalizedTranslationBundle localeBundle = new DefaultLocalizedTranslationBundle(this, locale);
     
             TranslationMessageParser parser = getTranslationMessageParser();
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/ComponentDocumentTranslationBundle.java+47 1 modified
    @@ -19,6 +19,8 @@
      */
     package org.xwiki.localization.wiki.internal;
     
    +import java.util.Locale;
    +
     import org.xwiki.bridge.event.WikiDeletedEvent;
     import org.xwiki.component.descriptor.ComponentDescriptor;
     import org.xwiki.component.manager.ComponentLookupException;
    @@ -27,6 +29,11 @@
     import org.xwiki.localization.message.TranslationMessageParser;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.observation.event.Event;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +
    +import com.xpn.xwiki.XWiki;
    +import com.xpn.xwiki.XWikiContext;
    +import com.xpn.xwiki.doc.XWikiDocument;
     
     /**
      * Component wiki document based implementation of Bundle.
    @@ -37,6 +44,8 @@
      */
     public class ComponentDocumentTranslationBundle extends AbstractDocumentTranslationBundle
     {
    +    private DocumentTranslationBundleFactory factory;
    +
         private ComponentDescriptor<TranslationBundle> descriptor;
     
         /**
    @@ -45,17 +54,54 @@ public class ComponentDocumentTranslationBundle extends AbstractDocumentTranslat
          * @param componentManager used to lookup components needed to manipulate wiki documents
          * @param translationMessageParser the parser to use for each message
          * @param descriptor the component descriptor used to unregister the bundle
    +     * @param factory the factory
          * @throws ComponentLookupException failed to lookup some required components
          */
         public ComponentDocumentTranslationBundle(String idPrefix, DocumentReference documentReference,
             ComponentManager componentManager, TranslationMessageParser translationMessageParser,
    -        ComponentDescriptor<TranslationBundle> descriptor) throws ComponentLookupException
    +        ComponentDescriptor<TranslationBundle> descriptor, DocumentTranslationBundleFactory factory)
    +        throws ComponentLookupException
         {
             super(idPrefix, documentReference, componentManager, translationMessageParser);
     
    +        this.factory = factory;
             this.descriptor = descriptor;
         }
     
    +    /**
    +     * {@inheritDoc}
    +     * This overrides the default implementation to first check the author rights for the document.
    +     *
    +     * @param locale the requested locale
    +     * @return the document defining the translation bundle if its author has the necessary rights, the default locale
    +     *     otherwise, and fallback on the original implementation if the document doesn't exist or could not be fetched
    +     */
    +    @Override
    +    protected XWikiDocument getDocumentLocaleBundle(Locale locale) throws Exception
    +    {
    +        XWikiDocument document = super.getDocumentLocaleBundle(locale);
    +
    +        if (document != null && !document.isNew()) {
    +            XWikiContext context = this.contextProvider.get();
    +            XWiki xwiki = context.getWiki();
    +            XWikiDocument defaultLocaleDocument = xwiki.getDocument(this.documentReference, context);
    +
    +            if (defaultLocaleDocument != document) {
    +                // We only need to check rights for non-default locales.
    +                try {
    +                    this.factory.checkRegistrationAuthorizationForDocumentLocaleBundle(document, defaultLocaleDocument);
    +                } catch (AccessDeniedException e) {
    +                    this.logger.warn("Failed to load and register the translation for locale [{}] from document [{}]. "
    +                        + "Falling back to default locale.", locale, document.getDocumentReference());
    +                    // We return the default translation bundle if the requested one has permission issues.
    +                    return defaultLocaleDocument;
    +                }
    +            }
    +        }
    +
    +        return document;
    +    }
    +
         @Override
         public void onEvent(Event event, Object source, Object data)
         {
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/java/org/xwiki/localization/wiki/internal/DocumentTranslationBundleFactory.java+29 5 modified
    @@ -56,6 +56,7 @@
     import org.xwiki.localization.wiki.internal.TranslationDocumentModel.Scope;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.model.reference.DocumentReferenceResolver;
    +import org.xwiki.model.reference.EntityReference;
     import org.xwiki.model.reference.EntityReferenceSerializer;
     import org.xwiki.model.reference.WikiReference;
     import org.xwiki.observation.EventListener;
    @@ -324,7 +325,7 @@ private ComponentDocumentTranslationBundle createComponentDocumentBundle(XWikiDo
             try {
                 documentBundle =
                     new ComponentDocumentTranslationBundle(ID_PREFIX, document.getDocumentReference(),
    -                    this.componentManagerProvider.get(), this.translationParser, descriptor);
    +                    this.componentManagerProvider.get(), this.translationParser, descriptor, this);
             } catch (ComponentLookupException e) {
                 throw new TranslationBundleDoesNotExistsException("Failed to create document bundle", e);
             }
    @@ -434,6 +435,23 @@ private void registerTranslationBundle(XWikiDocument document) throws Translatio
             }
         }
     
    +    /**
    +     * Checks that the author of a document defining a translation bundle has the necessary rights to make it
    +     * available, based on the scope of the default locale translation bundle.
    +     *
    +     * @param document the document defining the translation bundle to check
    +     * @param defaultLocaleDocument the document containing the default locale translation bundle
    +     * @throws AccessDeniedException when the document author does not have enough rights for the defined scope
    +     */
    +    protected void checkRegistrationAuthorizationForDocumentLocaleBundle(XWikiDocument document,
    +        XWikiDocument defaultLocaleDocument) throws AccessDeniedException
    +    {
    +        Scope scope = getScope(defaultLocaleDocument);
    +        if (scope != null && scope != Scope.ON_DEMAND) {
    +            checkRegistrationAuthorization(document, scope);
    +        }
    +    }
    +
         /**
          * @param document the translation document
          * @param scope the scope
    @@ -442,18 +460,24 @@ private void registerTranslationBundle(XWikiDocument document) throws Translatio
          */
         private void checkRegistrationAuthorization(XWikiDocument document, Scope scope) throws AccessDeniedException
         {
    +        EntityReference entityReference;
             switch (scope) {
                 case GLOBAL:
                     this.authorizationManager.checkAccess(Right.PROGRAM, document.getAuthorReference(), null);
    +                this.authorizationManager.checkAccess(Right.PROGRAM, document.getContentAuthorReference(), null);
                     break;
                 case WIKI:
    -                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), document
    -                    .getDocumentReference().getWikiReference());
    +                entityReference = document.getDocumentReference().getWikiReference();
    +                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), entityReference);
    +                this.authorizationManager.checkAccess(Right.ADMIN, document.getContentAuthorReference(),
    +                    entityReference);
                     break;
                 case USER:
                     if (this.configuration.isRestrictUserTranslations()) {
    -                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(),
    -                        document.getDocumentReference());
    +                    entityReference = document.getDocumentReference();
    +                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(), entityReference);
    +                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getContentAuthorReference(),
    +                        entityReference);
                     }
                     break;
                 default:
    
  • xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/test/java/org/xwiki/localization/wiki/internal/ComponentDocumentTranslationBundleTest.java+208 0 added
    @@ -0,0 +1,208 @@
    +/*
    + * See the NOTICE file distributed with this work for additional
    + * information regarding copyright ownership.
    + *
    + * This is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU Lesser General Public License as
    + * published by the Free Software Foundation; either version 2.1 of
    + * the License, or (at your option) any later version.
    + *
    + * This software is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    + * Lesser General Public License for more details.
    + *
    + * You should have received a copy of the GNU Lesser General Public
    + * License along with this software; if not, write to the Free
    + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
    + * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
    + */
    +package org.xwiki.localization.wiki.internal;
    +
    +import java.util.Collections;
    +import java.util.Locale;
    +
    +import javax.inject.Inject;
    +
    +import org.junit.jupiter.api.BeforeEach;
    +import org.junit.jupiter.api.Test;
    +import org.junit.jupiter.api.extension.RegisterExtension;
    +import org.mockito.Mock;
    +import org.xwiki.cache.Cache;
    +import org.xwiki.cache.CacheManager;
    +import org.xwiki.cache.config.CacheConfiguration;
    +import org.xwiki.component.internal.multi.ComponentManagerManager;
    +import org.xwiki.job.event.status.JobProgressManager;
    +import org.xwiki.localization.LocalizationManager;
    +import org.xwiki.localization.Translation;
    +import org.xwiki.localization.TranslationBundleFactory;
    +import org.xwiki.localization.internal.DefaultLocalizationManager;
    +import org.xwiki.localization.internal.DefaultTranslationBundleContext;
    +import org.xwiki.localization.messagetool.internal.MessageToolTranslationMessageParser;
    +import org.xwiki.model.internal.DefaultModelContext;
    +import org.xwiki.model.reference.DocumentReference;
    +import org.xwiki.observation.internal.DefaultObservationManager;
    +import org.xwiki.query.Query;
    +import org.xwiki.query.QueryManager;
    +import org.xwiki.rendering.internal.parser.plain.PlainTextBlockParser;
    +import org.xwiki.rendering.internal.renderer.plain.PlainTextBlockRenderer;
    +import org.xwiki.rendering.internal.renderer.plain.PlainTextRendererFactory;
    +import org.xwiki.rendering.syntax.Syntax;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +import org.xwiki.security.authorization.Right;
    +import org.xwiki.test.LogLevel;
    +import org.xwiki.test.annotation.BeforeComponent;
    +import org.xwiki.test.annotation.ComponentList;
    +import org.xwiki.test.junit5.LogCaptureExtension;
    +import org.xwiki.test.junit5.mockito.MockComponent;
    +
    +import com.xpn.xwiki.doc.XWikiDocument;
    +import com.xpn.xwiki.objects.BaseObject;
    +import com.xpn.xwiki.test.MockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
    +import com.xpn.xwiki.test.reference.ReferenceComponentList;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.mockito.ArgumentMatchers.anyString;
    +import static org.mockito.Mockito.any;
    +import static org.mockito.Mockito.doThrow;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.times;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.when;
    +
    +/**
    + * Unit tests for {@link ComponentDocumentTranslationBundle}.
    + *
    + * @version $Id$
    + */
    +@OldcoreTest
    +@ComponentList({
    +    DocumentTranslationBundleFactory.class,
    +    DefaultLocalizationManager.class,
    +    DefaultTranslationBundleContext.class,
    +    TranslationDocumentClassInitializer.class,
    +    DefaultModelContext.class,
    +    PlainTextBlockRenderer.class,
    +    PlainTextRendererFactory.class,
    +    DefaultObservationManager.class,
    +    MessageToolTranslationMessageParser.class,
    +    PlainTextBlockParser.class
    +})
    +@ReferenceComponentList
    +class ComponentDocumentTranslationBundleTest
    +{
    +    private static final DocumentReference TRANSLATION_ROOT_REFERENCE = new DocumentReference("xwiki", "space",
    +        "Translations");
    +
    +    private static final DocumentReference ADMIN_USER_REFERENCE = new DocumentReference("xwiki", "XWiki", "XWikiAdmin");
    +
    +    @InjectMockitoOldcore
    +    private MockitoOldcore oldcore;
    +
    +    @MockComponent
    +    private WikiTranslationConfiguration translationConfiguration;
    +
    +    @MockComponent
    +    private QueryManager mockQueryManager;
    +
    +    @Mock
    +    private Query mockQuery;
    +
    +    @MockComponent
    +    private ComponentManagerManager componentManagerManager;
    +
    +    @MockComponent
    +    private JobProgressManager jobProgressManager;
    +
    +    @Inject
    +    private LocalizationManager localization;
    +
    +    private XWikiDocument translationFrDocument;
    +
    +    private DocumentReference adminUserReference;
    +
    +    /**
    +     * Capture logs.
    +     */
    +    @RegisterExtension
    +    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
    +
    +    @MockComponent
    +    private CacheManager cacheManager;
    +
    +    @BeforeComponent
    +    void before() throws Exception
    +    {
    +        Cache<Object> cache = mock(Cache.class);
    +        when(this.cacheManager.createNewCache(any(CacheConfiguration.class))).thenReturn(cache);
    +    }
    +
    +    @BeforeEach
    +    void setUp() throws Exception
    +    {
    +        this.oldcore.notifyDocumentCreatedEvent(true);
    +
    +        when(this.mockQueryManager.createQuery(anyString(), anyString())).thenReturn(this.mockQuery);
    +        when(this.mockQuery.execute()).thenReturn(Collections.emptyList());
    +
    +        when(this.componentManagerManager.getComponentManager("wiki:xwiki", true)).thenReturn(this.oldcore.getMocker());
    +        this.oldcore.getMocker().getInstance(TranslationBundleFactory.class, DocumentTranslationBundleFactory.ID);
    +
    +        XWikiDocument translationRootDocument = this.oldcore.getSpyXWiki().getDocument(TRANSLATION_ROOT_REFERENCE,
    +            this.oldcore.getXWikiContext());
    +
    +        BaseObject translationObject = translationRootDocument.newXObject(
    +            new DocumentReference("xwiki", "XWiki", "TranslationDocumentClass"),
    +            this.oldcore.getXWikiContext());
    +        translationObject.setStringValue(TranslationDocumentModel.TRANSLATIONCLASS_PROP_SCOPE,
    +            TranslationDocumentModel.Scope.WIKI.toString());
    +
    +        translationRootDocument.setSyntax(Syntax.PLAIN_1_0);
    +        translationRootDocument.setContent("xwiki.translation=root");
    +        translationRootDocument.setAuthorReference(ADMIN_USER_REFERENCE);
    +        this.oldcore.getSpyXWiki().saveDocument(translationRootDocument, this.oldcore.getXWikiContext());
    +
    +        this.translationFrDocument = translationRootDocument.getTranslatedDocument(Locale.FRENCH,
    +            this.oldcore.getXWikiContext());
    +        if (this.translationFrDocument == translationRootDocument) {
    +            this.translationFrDocument =
    +                new XWikiDocument(this.translationFrDocument.getDocumentReference(), Locale.FRENCH);
    +            this.translationFrDocument.setDefaultLocale(this.translationFrDocument.getDefaultLocale());
    +        }
    +        this.translationFrDocument.setSyntax(Syntax.PLAIN_1_0);
    +        this.translationFrDocument.setContent("xwiki.translation=fr");
    +        this.oldcore.getSpyXWiki().saveDocument(this.translationFrDocument, this.oldcore.getXWikiContext());
    +
    +        doThrow(new AccessDeniedException(Right.SCRIPT, null, translationRootDocument.getDocumentReference()))
    +            .when(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.ADMIN, null,
    +                TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +
    +    @Test
    +    void checkTranslationWithExpectedRights() throws Exception
    +    {
    +        this.translationFrDocument.setAuthorReference(ADMIN_USER_REFERENCE);
    +        this.translationFrDocument.setContentAuthorReference(ADMIN_USER_REFERENCE);
    +        this.oldcore.getSpyXWiki().saveDocument(this.translationFrDocument, this.oldcore.getXWikiContext());
    +        Translation frTranslation = this.localization.getTranslation("xwiki.translation", Locale.FRENCH);
    +        assertEquals("fr", frTranslation.getRawSource());
    +        // Authorizations are checked twice because the mocked behavior is not actual locale bundle registration.
    +        verify(this.oldcore.getMockAuthorizationManager(), times(4)).checkAccess(Right.ADMIN, ADMIN_USER_REFERENCE,
    +            TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +
    +    @Test
    +    void checkTranslationWithoutExpectedRights() throws Exception
    +    {
    +        Translation frTranslation = this.localization.getTranslation("xwiki.translation", Locale.FRENCH);
    +        assertEquals(
    +            "Failed to load and register the translation for locale [fr] from document [xwiki:space.Translations]. "
    +                + "Falling back to default locale.",
    +            this.logCapture.getMessage(0));
    +        assertEquals("root", frTranslation.getRawSource());
    +        verify(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.ADMIN, null,
    +            TRANSLATION_ROOT_REFERENCE.getWikiReference());
    +    }
    +}
    

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

7

News mentions

0

No linked articles in our index yet.