CVE-2024-31983 (Java, published 2024-04-10T20:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
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
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
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
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
Commit message: XWIKI-21411: Improve check of translation document author rights

* Check both author and content author and improve JavaDoc


Vulnerable code:
    protected LocalizedTranslationBundle loadDocumentLocaleBundle(Locale locale) throws Exception
            // No context for some reason, lets try later
            // No XWiki instance ready, lets try later
            if (document.isNew()) {
                // No document found for this locale
                return LocalizedTranslationBundle.EMPTY;
            }
        // Convert to LocalBundle
        ComponentDescriptor<TranslationBundle> descriptor) throws ComponentLookupException
                    this.componentManagerProvider.get(), this.translationParser, descriptor);
                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), document
                    .getDocumentReference().getWikiReference());
                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(),
                        document.getDocumentReference());

Fixed code:
    /**
     * 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
            // No context for some reason, let's try later.
            // No XWiki instance ready, let's try later.
        }
        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;
        // Convert to LocalBundle.
import java.util.Locale;

import org.xwiki.security.authorization.AccessDeniedException;

import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
    private DocumentTranslationBundleFactory factory;

     * @param factory the factory
        ComponentDescriptor<TranslationBundle> descriptor, DocumentTranslationBundleFactory factory)
        throws ComponentLookupException
        this.factory = factory;
    /**
     * {@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;
    }

import org.xwiki.model.reference.EntityReference;
                    this.componentManagerProvider.get(), this.translationParser, descriptor, this);
    /**
     * 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);
        }
    }

        EntityReference entityReference;
                this.authorizationManager.checkAccess(Right.PROGRAM, document.getContentAuthorReference(), null);
                entityReference = document.getDocumentReference().getWikiReference();
                this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), entityReference);
                this.authorizationManager.checkAccess(Right.ADMIN, document.getContentAuthorReference(),
                    entityReference);
                    entityReference = document.getDocumentReference();
                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(), entityReference);
                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getContentAuthorReference(),
                        entityReference);
/*
 * 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());
    }
}