CVE-2023-29213 (XML, published 2023-04-17T22:15Z)
Description: value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. In affected versions of `org.xwiki.platform:xwiki-platform-logging-ui` it is possible to trick a user with programming rights into visiting a constructed url where e.g., by embedding an image with this URL in a document that is viewed by a user with programming rights which will evaluate an expression in the constructed url and execute it. This issue has been addressed in versions 13.10.11, 14.4.7, and 14.10. Users are advised to upgrade. There are no known workarounds for this vulnerability.

Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-script/pom.xml
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-script/src/test/java/org/xwiki/logging/LoggingScriptServiceComponentList.java
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-script/src/test/java/org/xwiki/logging/NullLoggerManager.java
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-ui/pom.xml
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-ui/src/main/resources/XWiki/Logging/Code/Translations.xml
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-ui/src/main/resources/XWiki/LoggingAdmin.xml
xwiki-platform-core/xwiki-platform-logging/xwiki-platform-logging-ui/src/test/java/org/xwiki/logging/LoggingAdminPageTest.java
xwiki-platform-core/xwiki-platform-test/xwiki-platform-test-page/src/main/java/org/xwiki/test/page/LocalizationSetup.java
Commit message: XWIKI-20291: Improved translations of logging administration


Vulnerable code:
#if ($request.loggeraction_set &amp;&amp; $request.logger_name &amp;&amp; $xwiki.hasAccessLevel('programming'))
  #if ($!logger_list.containsKey($request.logger_name))
      {{success}}Logger ${request.logger_name} level has been unset.{{/success}}
      #set($void = $logging.setLevel($request.logger_name, $null))
      {{success}}Logger ${request.logger_name} level has been set to ${request.logger_level}.{{/success}}
      #set($void = $logging.setLevel($request.logger_name, $request.logger_level))
    {{error}}Failed to set log level: the logger "$request.logger_name" doesn't exist.{{/error}}

        if (invocationOnMockRender.getArguments().length > i) {
            Object argument = invocationOnMockRender.getArgument(i);
            if (argument instanceof String) {
                parameters = new Object[] { argument };
            } else {
                parameters = (Object[]) argument;
            }

Fixed code:
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <executions>
          <execution>
            <id>test-jar</id>
            <goals>
              <goal>test-jar</goal>
            </goals>
            <configuration>
              <includes>
                <include>**/LoggingScriptServiceComponentList.class</include>
                <include>**/NullLoggerManager.class</include>
              </includes>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
/*
 * 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.logging;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import org.xwiki.logging.script.LoggingScriptService;
import org.xwiki.test.annotation.ComponentList;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Component list for {@link LoggingScriptService}.
 * 
 * @version $Id$
 * @since 13.10.11
 * @since 14.4.7
 * @since 14.10RC1
 */
@Documented
@Retention(RUNTIME)
@Target({ TYPE, METHOD, ANNOTATION_TYPE })
@ComponentList({
    LoggingScriptService.class,
    NullLoggerManager.class
})
@Inherited
public @interface LoggingScriptServiceComponentList
{
}
/*
 * 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.logging;

import java.util.Collection;

import org.slf4j.Logger;
import org.xwiki.observation.EventListener;

/**
 * Null logger manager, to be overridden if need be.
 * 
 * @version $Id$
 * @since 13.10.11
 * @since 14.4.7
 * @since 14.10RC1
 */
public class NullLoggerManager implements LoggerManager
{
    @Override
    public void pushLogListener(EventListener listener)
    {
        
    }

    @Override
    public EventListener popLogListener()
    {
        return null;
    }

    @Override
    public void setLoggerLevel(String loggerName, LogLevel level)
    {

    }

    @Override
    public LogLevel getLoggerLevel(String loggerName)
    {
        return null;
    }

    @Override
    public Collection<Logger> getLoggers()
    {
        return null;
    }
}
    <!-- Test dependencies. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-test-page</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
    <!-- Provides the component list for RenderingScriptService. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    <!-- Provides the component list for the LiveData macro. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-livedata-macro</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
    <!-- Required for LoggingScriptService. -->
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-logging-script</artifactId>
      <version>${project.version}</version>
      <type>test-jar</type>
      <scope>test</scope>
    </dependency>
<?xml version="1.1" encoding="UTF-8"?>

<!--
 * 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.
-->

<xwikidoc version="1.5" reference="XWiki.Logging.Code.Translations" locale="">
  <web>XWiki.Logging.Code</web>
  <name>Translations</name>
  <language/>
  <defaultLanguage>en</defaultLanguage>
  <translation>0</translation>
  <creator>xwiki:XWiki.Admin</creator>
  <parent>WebHome</parent>
  <author>xwiki:XWiki.Admin</author>
  <contentAuthor>xwiki:XWiki.Admin</contentAuthor>
  <version>1.1</version>
  <title>Translations</title>
  <comment/>
  <minorEdit>false</minorEdit>
  <syntaxId>plain/1.0</syntaxId>
  <hidden>true</hidden>
  <content>logging.admin.unsetLevel.success=Logger "{0}" level has been unset.
logging.admin.setLevel.success=Logger "{0}" level has been set to "{1}".
logging.admin.setLevel.error=Failed to set log level: the logger "{0}" doesn't exist.</content>
  <object>
    <name>XWiki.Logging.Code.Translations</name>
    <number>0</number>
    <className>XWiki.TranslationDocumentClass</className>
    <guid>fc29bfac-011a-4573-80ba-3f5713f1e847</guid>
    <class>
      <name>XWiki.TranslationDocumentClass</name>
      <customClass/>
      <customMapping/>
      <defaultViewSheet/>
      <defaultEditSheet/>
      <defaultWeb/>
      <nameField/>
      <validationScript/>
      <scope>
        <cache>0</cache>
        <disabled>0</disabled>
        <displayType>select</displayType>
        <freeText>forbidden</freeText>
        <largeStorage>0</largeStorage>
        <multiSelect>0</multiSelect>
        <name>scope</name>
        <number>1</number>
        <prettyName>Scope</prettyName>
        <relationalStorage>0</relationalStorage>
        <separator> </separator>
        <separators>|, </separators>
        <size>1</size>
        <unmodifiable>0</unmodifiable>
        <values>GLOBAL|WIKI|USER|ON_DEMAND</values>
        <classType>com.xpn.xwiki.objects.classes.StaticListClass</classType>
      </scope>
    </class>
    <property>
      <scope>WIKI</scope>
    </property>
  </object>
</xwikidoc>
#set ($loggerName = $request.logger_name)
#set ($loggerNameEscaped = $services.rendering.escape($escapetool.java($loggerName), 'xwiki/2.1'))
#set ($loggerLevel = $request.logger_level)
#set ($loggerLevelEscaped = $services.rendering.escape($escapetool.java($loggerLevel), 'xwiki/2.1'))
#if ($request.loggeraction_set &amp;&amp; $loggerName &amp;&amp; $xwiki.hasAccessLevel('programming'))
  #if ($!logger_list.containsKey($loggerName))
      {{success}}
        {{translation key='logging.admin.unsetLevel.success' parameters="~"${loggerNameEscaped}~""/}}
      {{/success}}
      #set($void = $logging.setLevel($loggerName, $null))
      {{success}}
        {{translation key='logging.admin.setLevel.success'
          parameters="~"${loggerNameEscaped}~",~"${loggerLevelEscaped}~""/}}
      {{/success}}
      #set($void = $logging.setLevel($loggerName, $request.logger_level))
    {{error}}{{translation key='logging.admin.setLevel.error' parameters="~"${loggerNameEscaped}~""/}}{{/error}}

/*
 * 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.logging;

import java.util.HashMap;

import org.jsoup.nodes.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xwiki.icon.IconManager;
import org.xwiki.livedata.internal.macro.LiveDataMacroComponentList;
import org.xwiki.localization.macro.internal.TranslationMacro;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.properties.internal.converter.EnumConverter;
import org.xwiki.rendering.RenderingScriptServiceComponentList;
import org.xwiki.rendering.internal.configuration.DefaultExtendedRenderingConfiguration;
import org.xwiki.rendering.internal.configuration.RenderingConfigClassDocumentConfigurationSource;
import org.xwiki.rendering.internal.macro.message.ErrorMessageMacro;
import org.xwiki.rendering.internal.macro.message.SuccessMessageMacro;
import org.xwiki.security.authorization.ContextualAuthorizationManager;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.page.HTML50ComponentList;
import org.xwiki.test.page.PageTest;
import org.xwiki.test.page.XWikiSyntax21ComponentList;

import com.xpn.xwiki.plugin.skinx.SkinExtensionPluginApi;

import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Test of {@code XWiki.LoggingAdmin}.
 *
 * @version $Id$
 * @since 13.10.11
 * @since 14.4.7
 * @since 14.10RC1
 */
@XWikiSyntax21ComponentList
@HTML50ComponentList
@RenderingScriptServiceComponentList
@LiveDataMacroComponentList
@LoggingScriptServiceComponentList
@ComponentList({
    TranslationMacro.class,
    DefaultExtendedRenderingConfiguration.class,
    RenderingConfigClassDocumentConfigurationSource.class,
    ErrorMessageMacro.class,
    SuccessMessageMacro.class,
    EnumConverter.class
})
class LoggingAdminPageTest extends PageTest
{
    private LoggerManager loggerManager;

    @BeforeEach
    void setUp() throws Exception
    {
        // Spy the jsfx plugin used during the macro rendering to return a mock of its API when required. 
        when(this.oldcore.getSpyXWiki().getPluginApi("jsfx", this.context))
            .thenReturn(mock(SkinExtensionPluginApi.class));
        // Return minimal icons metadata since this is not what we want to test here.
        IconManager iconManager = this.componentManager.registerMockComponent(IconManager.class);
        doReturn(new HashMap<>()).when(iconManager).getMetaData(anyString());
        // Setting all rights as allowed by default.
        when(this.xwiki.getRightService().hasAccessLevel(any(), any(), anyString(), eq(this.context)))
            .thenReturn(true);
        when(this.componentManager.<ContextualAuthorizationManager>getInstance(ContextualAuthorizationManager.class)
            .hasAccess(any())).thenReturn(true);
        // Register a mocked LoggerManager as we want to control the logger level. 
        this.loggerManager = this.componentManager.registerMockComponent(LoggerManager.class);
        Logger loggerA = mock(Logger.class);
        when(loggerA.getName()).thenReturn("loggerA");
        when(this.loggerManager.getLoggers()).thenReturn(asList(loggerA));
    }

    @Test
    void unknownLogger() throws Exception
    {
        this.request.put("loggeraction_set", "1");
        this.request.put("logger_name", "{{cache}}{{groovy}}new File(\"/tmp/exploit.txt\").withWriter { out -> "
            + "out.println(\"created from notification filter preferences!\"); "
            + "}{{/groovy}}{{/cache}}<strong>bold</strong>\"'");
        this.request.put("logger_level", "TRACE");
        Document document = renderHTMLPage(new DocumentReference("xwiki", "XWiki", "LoggingAdmin"));
        assertEquals("logging.admin.setLevel.error [{{cache}}{{groovy}}new File(\"/tmp/exploit.txt\")"
            + ".withWriter { out -> out.println(\"created from notification filter preferences!\"); }{{/groovy}}"
            + "{{/cache}}<strong>bold</strong>\"']", document.select(".box.errormessage").text());
        verify(this.loggerManager, never()).setLoggerLevel(any(), any());
    }

    @Test
    void unsetLogger() throws Exception
    {
        this.request.put("loggeraction_set", "1");
        this.request.put("logger_name", "loggerA");
        this.request.put("logger_level", "");
        Document document = renderHTMLPage(new DocumentReference("xwiki", "XWiki", "LoggingAdmin"));
        assertEquals("logging.admin.unsetLevel.success [loggerA]", document.select(".box.successmessage").text());
        verify(this.loggerManager).setLoggerLevel("loggerA", null);
    }

    @Test
    void setLogger() throws Exception
    {
        this.request.put("loggeraction_set", "1");
        this.request.put("logger_name", "loggerA");
        this.request.put("logger_level", "TRACE");
        Document document = renderHTMLPage(new DocumentReference("xwiki", "XWiki", "LoggingAdmin"));
        assertEquals("logging.admin.setLevel.success [loggerA, TRACE]", document.select(".box.successmessage").text());
        verify(this.loggerManager).setLoggerLevel("loggerA", LogLevel.TRACE);
    }
}
        Object[] arguments = invocationOnMockRender.getArguments();
        if (arguments.length > i) {
            parameters = Arrays.copyOfRange(arguments, i, arguments.length);