CVE-2023-29510: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
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.
CWE description: 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.
CVSS v3 base score: 8.8
Published: 2023-04-19T00:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
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 date: 2022-12-19 17:40:06 +0100
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.

Diff: diff --git a/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 b/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
new file mode 100644
index 0000000000000..0f8f3de87c8b1
--- /dev/null
+++ b/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
@@ -0,0 +1,51 @@
+/*
+ * 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);
+    }
+}
diff --git a/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 b/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
index cc556e31a4d0b..d5b82f48ff008 100644
--- a/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
+++ b/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
@@ -141,10 +141,13 @@ public class DocumentTranslationBundleFactory implements TranslationBundleFactor
     private QueryManager queryManager;
 
     @Inject
     private AuthorizationManager authorizationManager;
 
+    @Inject
+    private WikiTranslationConfiguration configuration;
+
     /**
      * Used to cache on demand document bundles (those that are not registered as components).
      */
     private Cache<TranslationBundle> onDemandBundleCache;
 
@@ -445,10 +448,16 @@ private void checkRegistrationAuthorization(XWikiDocument document, Scope scope)
                 break;
             case WIKI:
                 this.authorizationManager.checkAccess(Right.ADMIN, document.getAuthorReference(), document
                     .getDocumentReference().getWikiReference());
                 break;
+            case USER:
+                if (this.configuration.isRestrictUserTranslations()) {
+                    this.authorizationManager.checkAccess(Right.SCRIPT, document.getAuthorReference(),
+                        document.getDocumentReference());
+                }
+                break;
             default:
                 break;
         }
     }
 
diff --git a/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 b/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
new file mode 100644
index 0000000000000..8abca414f04d7
--- /dev/null
+++ b/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
@@ -0,0 +1,38 @@
+/*
+ * 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();
+}
diff --git a/xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/resources/META-INF/components.txt
index 356355e52e898..f6c9b05ee3437 100644
--- a/xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/resources/META-INF/components.txt
+++ b/xwiki-platform-core/xwiki-platform-localization/xwiki-platform-localization-sources/xwiki-platform-localization-source-wiki/src/main/resources/META-INF/components.txt
@@ -1,3 +1,4 @@
+org.xwiki.localization.wiki.internal.DefaultWikiTranslationConfiguration
 org.xwiki.localization.wiki.internal.DocumentTranslationBundleFactory
 org.xwiki.localization.wiki.internal.DocumentTranslationBundleInitializer
 org.xwiki.localization.wiki.internal.TranslationDocumentClassInitializer
diff --git a/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 b/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
index 4383d7323d498..59e14d6df991a 100644
--- a/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
+++ b/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
@@ -20,99 +20,148 @@
 package org.xwiki.localization.wiki.internal;
 
 import java.util.Collections;
 import java.util.Locale;
 
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+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.mockito.Mockito;
+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.ComponentLookupException;
-import org.xwiki.configuration.ConfigurationSource;
+import org.xwiki.component.manager.ComponentManager;
 import org.xwiki.localization.LocalizationManager;
 import org.xwiki.localization.Translation;
 import org.xwiki.localization.TranslationBundleDoesNotExistsException;
 import org.xwiki.localization.TranslationBundleFactory;
 import org.xwiki.localization.TranslationBundleFactoryDoesNotExistsException;
+import org.xwiki.localization.internal.DefaultLocalizationManager;
 import org.xwiki.localization.internal.DefaultTranslationBundleContext;
+import org.xwiki.localization.messagetool.internal.MessageToolTranslationMessageParser;
 import org.xwiki.localization.wiki.internal.TranslationDocumentModel.Scope;
+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.test.annotation.AfterComponent;
-import org.xwiki.test.annotation.AllComponents;
+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 org.xwiki.wiki.descriptor.WikiDescriptorManager;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.objects.BaseObject;
-import com.xpn.xwiki.test.MockitoOldcoreRule;
-
+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.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-@AllComponents
-public class DocumentTranslationBundleFactoryTest
+/**
+ * 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
 {
-    @Rule
-    public MockitoOldcoreRule oldcore = new MockitoOldcoreRule();
+    @InjectMockitoOldcore
+    private MockitoOldcore oldcore;
 
+    @MockComponent
+    private WikiTranslationConfiguration translationConfiguration;
+
+    @MockComponent
     private QueryManager mockQueryManager;
 
+    @Mock
     private Query mockQuery;
 
+    @MockComponent
     private WikiDescriptorManager mockWikiDescriptorManager;
 
+    @MockComponent
+    private ComponentManagerManager componentManagerManager;
+
+    @Inject
     private LocalizationManager localization;
 
-    public DocumentTranslationBundleFactoryTest()
+    /**
+     * Capture logs.
+     */
+    @RegisterExtension
+    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
+
+    @BeforeEach
+    public void before() throws Exception
     {
         this.oldcore.notifyDocumentCreatedEvent(true);
         this.oldcore.notifyDocumentUpdatedEvent(true);
         this.oldcore.notifyDocumentDeletedEvent(true);
-    }
 
-    @Before
-    public void before() throws Exception
-    {
         this.oldcore.getXWikiContext().setMainXWiki("xwiki");
         this.oldcore.getXWikiContext().setWikiId("xwiki");
 
         doReturn("plain/1.0").when(this.oldcore.getSpyXWiki()).getCurrentContentSyntaxId(Mockito.any(String.class),
             Mockito.any(XWikiContext.class));
 
-        this.mockQuery = mock(Query.class);
-
         when(this.mockQueryManager.createQuery(Mockito.any(String.class), Mockito.any(String.class)))
             .thenReturn(this.mockQuery);
-        when(this.mockQuery.execute()).thenReturn(Collections.EMPTY_LIST);
+        when(this.mockQuery.execute()).thenReturn(Collections.emptyList());
 
         when(this.mockWikiDescriptorManager.getMainWikiId()).thenReturn(this.oldcore.getXWikiContext().getMainXWiki());
         when(this.mockWikiDescriptorManager.getCurrentWikiId()).thenReturn(this.oldcore.getXWikiContext().getWikiId());
 
+        // 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());
+
         // Initialize document bundle factory
         this.oldcore.getMocker().getInstance(TranslationBundleFactory.class, DocumentTranslationBundleFactory.ID);
 
-        this.localization = this.oldcore.getMocker().getInstance(LocalizationManager.class);
-
-        this.oldcore.getMocker().registerMockComponent(ConfigurationSource.class);
-
         // We want to be notified about new components registrations
         this.oldcore.notifyComponentDescriptorEvent();
     }
 
-    @AfterComponent
-    public void registerComponents() throws Exception
-    {
-        this.mockQueryManager = this.oldcore.getMocker().registerMockComponent(QueryManager.class);
-        this.mockWikiDescriptorManager = this.oldcore.getMocker().registerMockComponent(WikiDescriptorManager.class);
-    }
-
     private void addTranslation(String key, String message, DocumentReference reference, Locale locale, Scope scope)
         throws XWikiException
     {
         XWikiDocument document = this.oldcore.getSpyXWiki().getDocument(reference, this.oldcore.getXWikiContext());
 
@@ -140,31 +189,29 @@ private void addTranslation(String key, String message, DocumentReference refere
             document = tdocument;
         }
 
         document.setSyntax(Syntax.PLAIN_1_0);
 
-        StringBuilder builder = new StringBuilder(document.getContent());
+        String content = document.getContent() + '\n'
+            + key
+            + '='
+            + message;
 
-        builder.append('\n');
-        builder.append(key);
-        builder.append('=');
-        builder.append(message);
-
-        document.setContent(builder.toString());
+        document.setContent(content);
 
         this.oldcore.getSpyXWiki().saveDocument(document, "", this.oldcore.getXWikiContext());
     }
 
     private void assertTranslation(String key, String message, Locale locale)
     {
         Translation translation = this.localization.getTranslation(key, locale);
 
         if (message != null) {
-            Assert.assertNotNull("No translation could be found for key [" + key + "]", translation);
-            Assert.assertEquals(message, translation.getRawSource());
+            assertNotNull(translation, "No translation could be found for key [" + key + "]");
+            assertEquals(message, translation.getRawSource());
         } else {
-            Assert.assertNull(translation);
+            assertNull(translation);
         }
     }
 
     private void resetContext() throws ComponentLookupException
     {
@@ -172,11 +219,11 @@ private void resetContext() throws ComponentLookupException
     }
 
     // tests
 
     @Test
-    public void getTranslationScopeWiki() throws XWikiException, ComponentLookupException
+    void getTranslationScopeWiki() throws XWikiException, ComponentLookupException
     {
         assertTranslation("wiki.translation", null, Locale.ROOT);
 
         addTranslation("wiki.translation", "Wiki translation",
             new DocumentReference(this.oldcore.getXWikiContext().getWikiId(), "space", "translation"), Locale.ROOT,
@@ -189,11 +236,11 @@ public void getTranslationScopeWiki() throws XWikiException, ComponentLookupExce
 
         assertTranslation("wiki.translation", "Wiki translation", Locale.ROOT);
     }
 
     @Test
-    public void getTranslationScopeWikiFromOtherWiki() throws XWikiException, ComponentLookupException
+    void getTranslationScopeWikiFromOtherWiki() throws XWikiException, ComponentLookupException
     {
         assertTranslation("wiki.translation", null, Locale.ROOT);
 
         addTranslation("wiki.translation", "Wiki translation",
             new DocumentReference("otherwiki", "space", "translation"), Locale.ROOT, Scope.WIKI);
@@ -205,11 +252,11 @@ public void getTranslationScopeWikiFromOtherWiki() throws XWikiException, Compon
 
         assertTranslation("wiki.translation", null, Locale.ROOT);
     }
 
     @Test
-    public void getTranslationScopeONDemand() throws XWikiException, TranslationBundleDoesNotExistsException,
+    void getTranslationScopeONDemand() throws XWikiException, TranslationBundleDoesNotExistsException,
         TranslationBundleFactoryDoesNotExistsException, ComponentLookupException
     {
         assertTranslation("wiki.translation", null, Locale.ROOT);
 
         DocumentReference translationDocument =
@@ -222,6 +269,29 @@ public void getTranslationScopeONDemand() throws XWikiException, TranslationBund
 
         this.localization.use(DocumentTranslationBundleFactory.ID, translationDocument.toString());
 
         assertTranslation("wiki.translation", "Wiki translation", Locale.ROOT);
     }
+
+    @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);
+    }
 }
diff --git a/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm b/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm
index f8a5c12f62fe6..c9f90a1744e70 100644
--- a/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm
+++ b/xwiki-platform-tools/xwiki-platform-tool-configuration-resources/src/main/resources/xwiki.properties.vm
@@ -1389,6 +1389,16 @@ edit.defaultEditor.org.xwiki.rendering.block.XDOM#wysiwyg=$xwikiPropertiesDefaul
 #-#   follow the ECMAScript strict rules. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode
 #-#
 #-# The default value is:
 # skinx.jsStrictModeEnabled = false
 
+#-------------------------------------------------------------------------------------
+# 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
+
 $!xwikiPropertiesAdditionalProperties

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