CVE-2023-29510 (Java, published 2023-04-19T00:15Z)
Description: value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. In XWiki, every user can add translations that are only applied to the current user. This also allows overriding existing translations. Such translations are often included in privileged contexts without any escaping which allows remote code execution for any user who has edit access on at least one document which could be the user's own profile where edit access is enabled by default. A mitigation for this vulnerability is part of XWiki 14.10.2 and XWiki 15.0 RC1: translations with user scope now require script right. This means that regular users cannot exploit this anymore as users don't have script right by default anymore starting with XWiki 14.10. There are no known workarounds apart from upgrading to a patched versions.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
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/DefaultWikiTranslationConfiguration.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/main/java/org/xwiki/localization/wiki/internal/WikiTranslationConfiguration.java
xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/resources/META-INF/components.txt
xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/test/java/org/xwiki/localization/wiki/internal/DocumentTranslationBundleFactoryTest.java
xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm
Commit message: XWIKI-19749: Require script right for translations with user scope

* Add a new configuration option.
* Migrate tests to JUnit 5 and add a new test.


Vulnerable code:
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.test.annotation.AfterComponent;
import org.xwiki.test.annotation.AllComponents;
import com.xpn.xwiki.test.MockitoOldcoreRule;

@AllComponents
public class DocumentTranslationBundleFactoryTest
    @Rule
    public MockitoOldcoreRule oldcore = new MockitoOldcoreRule();
    public DocumentTranslationBundleFactoryTest()
    }
    @Before
    public void before() throws Exception
    {
        this.mockQuery = mock(Query.class);

        when(this.mockQuery.execute()).thenReturn(Collections.EMPTY_LIST);
        this.localization = this.oldcore.getMocker().getInstance(LocalizationManager.class);

        this.oldcore.getMocker().registerMockComponent(ConfigurationSource.class);

    @AfterComponent
    public void registerComponents() throws Exception
    {
        this.mockQueryManager = this.oldcore.getMocker().registerMockComponent(QueryManager.class);
        this.mockWikiDescriptorManager = this.oldcore.getMocker().registerMockComponent(WikiDescriptorManager.class);
    }

        StringBuilder builder = new StringBuilder(document.getContent());
        builder.append('\n');
        builder.append(key);
        builder.append('=');
        builder.append(message);

        document.setContent(builder.toString());
            Assert.assertNotNull("No translation could be found for key [" + key + "]", translation);
            Assert.assertEquals(message, translation.getRawSource());
            Assert.assertNull(translation);
    public void getTranslationScopeWiki() throws XWikiException, ComponentLookupException
    public void getTranslationScopeWikiFromOtherWiki() throws XWikiException, ComponentLookupException
    public void getTranslationScopeONDemand() throws XWikiException, TranslationBundleDoesNotExistsException,

Fixed code:
/*
 * 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 javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.configuration.ConfigurationSource;

/**
 * Default implementation of the {@link WikiTranslationConfiguration}.
 *
 * @since 14.10.2
 * @since 15.0RC1
 * @version $Id$
 */
@Singleton
@Component
public class DefaultWikiTranslationConfiguration implements WikiTranslationConfiguration
{
    private static final String PREFIX = "localization.wiki.";

    @Inject
    @Named("xwikiproperties")
    private ConfigurationSource configuration;

    @Override
    public boolean isRestrictUserTranslations()
    {
        return this.configuration.getProperty(PREFIX + "restrictUserTranslations", true);
    }
}
    @Inject
    private WikiTranslationConfiguration configuration;

            case USER:
                if (this.configuration.isRestrictUserTranslations()) {
                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(),
                        document.getDocumentReference());
                }
                break;
/*
 * 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 org.xwiki.component.annotation.Role;

/**
 * Configuration of the wiki translations.
 *
 * @since 14.10.2
 * @since 15.0RC1
 * @version $Id$
 */
@Role
public interface WikiTranslationConfiguration
{
    /**
     * @return if translations with user scope shall be restricted to users with script right
     */
    boolean isRestrictUserTranslations();
}
org.xwiki.localization.wiki.internal.DefaultWikiTranslationConfiguration
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.infinispan.internal.InfinispanCacheFactory;
import org.xwiki.cache.internal.DefaultCacheManager;
import org.xwiki.cache.internal.DefaultCacheManagerConfiguration;
import org.xwiki.component.internal.multi.ComponentManagerManager;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.localization.internal.DefaultLocalizationManager;
import org.xwiki.localization.messagetool.internal.MessageToolTranslationMessageParser;
import org.xwiki.model.internal.DefaultModelContext;
import org.xwiki.observation.internal.DefaultObservationManager;
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.security.authorization.AccessDeniedException;
import org.xwiki.security.authorization.Right;
import org.xwiki.test.LogLevel;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.junit5.LogCaptureExtension;
import org.xwiki.test.junit5.mockito.MockComponent;
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.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.doThrow;
/**
 * Unit tests for {@link DocumentTranslationBundleFactory}.
 *
 * @version $Id$
 */
@OldcoreTest
@ComponentList({
    DocumentTranslationBundleFactory.class,
    DefaultLocalizationManager.class,
    DefaultTranslationBundleContext.class,
    DefaultModelContext.class,
    PlainTextBlockRenderer.class,
    PlainTextRendererFactory.class,
    DefaultObservationManager.class,
    DefaultCacheManager.class,
    DefaultCacheManagerConfiguration.class,
    MessageToolTranslationMessageParser.class,
    PlainTextBlockParser.class,
    InfinispanCacheFactory.class })
@ReferenceComponentList
class DocumentTranslationBundleFactoryTest
    @InjectMockitoOldcore
    private MockitoOldcore oldcore;
    @MockComponent
    private WikiTranslationConfiguration translationConfiguration;

    @MockComponent
    @Mock
    @MockComponent
    @MockComponent
    private ComponentManagerManager componentManagerManager;

    @Inject
    /**
     * Capture logs.
     */
    @RegisterExtension
    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

    @BeforeEach
    public void before() throws Exception
        when(this.mockQuery.execute()).thenReturn(Collections.emptyList());
        // Return the "context" component manager for the current wiki and the current user but not for another wiki.
        when(this.componentManagerManager.getComponentManager("wiki:xwiki", true)).thenReturn(this.oldcore.getMocker());
        when(this.componentManagerManager.getComponentManager("wiki:otherwiki", true))
            .thenReturn(mock(ComponentManager.class));
        when(this.componentManagerManager.getComponentManager("user:null", true)).thenReturn(this.oldcore.getMocker());

        String content = document.getContent() + '\n'
            + key
            + '='
            + message;
        document.setContent(content);
            assertNotNull(translation, "No translation could be found for key [" + key + "]");
            assertEquals(message, translation.getRawSource());
            assertNull(translation);
    void getTranslationScopeWiki() throws XWikiException, ComponentLookupException
    void getTranslationScopeWikiFromOtherWiki() throws XWikiException, ComponentLookupException
    void getTranslationScopeONDemand() throws XWikiException, TranslationBundleDoesNotExistsException,

    @Test
    void restrictUserTranslations() throws XWikiException, AccessDeniedException
    {
        DocumentReference translationDocument =
            new DocumentReference(this.oldcore.getXWikiContext().getWikiId(), "space", "translation");

        when(this.translationConfiguration.isRestrictUserTranslations()).thenReturn(true);

        addTranslation("user.translation", "User translation", translationDocument, Locale.ROOT, Scope.USER);

        assertTranslation("user.translation", "User translation", Locale.ROOT);

        doThrow(new AccessDeniedException(Right.SCRIPT, null, translationDocument))
            .when(this.oldcore.getMockAuthorizationManager()).checkAccess(Right.SCRIPT, null, translationDocument);

        addTranslation("user.translation2", "User translation", translationDocument, Locale.ROOT, Scope.USER);

        assertEquals("Failed to register translation bundle from document [xwiki:space.translation]",
            this.logCapture.getMessage(0));

        assertTranslation("user.translation", null, Locale.ROOT);
    }
#-------------------------------------------------------------------------------------
# Localization
#-------------------------------------------------------------------------------------

#-# [Since 14.10.2, 15.0RC1]
#-# Indicate whether translations with user scope that are only applied for the user who created them shall be
#-# restricted to users with script right. The default value is true. Disable this option only when you absolutely
#-# trust all users on the wiki.
# localization.wiki.restrictUserTranslations = true