CVE-2023-36470 (Java, published 2023-06-29T21:15Z)
Description: value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. By either creating a new or editing an existing document with an icon set, an attacker can inject XWiki syntax and Velocity code that is executed with programming rights and thus allows remote code execution. There are different attack vectors, the simplest is the Velocity code in the icon set's HTML or XWiki syntax definition. The [icon picker](https://extensions.xwiki.org/xwiki/bin/view/Extension/Icon%20Theme%20Application#HIconPicker) can be used to trigger the rendering of any icon set. The XWiki syntax variant of the icon set is also used without any escaping in some documents, allowing to inject XWiki syntax including script macros into a document that might have programming right, for this the currently used icon theme needs to be edited. Further, the HTML output of the icon set is output as JSON in the icon picker and this JSON is interpreted as XWiki syntax, allowing again the injection of script macros into a document with programming right and thus allowing remote code execution. This impacts the confidentiality, integrity and availability of the whole XWiki instance. This issue has been patched in XWiki 14.10.6 and 15.1. Icon themes now require script right and the code in the icon theme is executed within the context of the icon theme, preventing any rights escalation. A macro for displaying icons has been introduced to avoid injecting the raw wiki syntax of an icon set into another document. Users are advised to upgrade. There are no known workarounds for this vulnerability.

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-bridge/pom.xml
xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutor.java
xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DocumentContextExecutor.java
xwiki-platform-core/xwiki-platform-bridge/src/main/resources/META-INF/components.txt
xwiki-platform-core/xwiki-platform-bridge/src/test/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutorTest.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/internal/util/XWikiSyntaxEscaper.java
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java
Commit message: XWIKI-20524: Improve icon rendering

* Require script right for icon set authors.
* Record the icon set's source document reference in the icon theme.
* Refactor XWiki syntax escaping into its own component. Currently, this
  component is internal as this is not the right place for it.
* Execute the Velocity code in the context of the icon theme's source
  document.
* Introduce a version of IconException that just takes a string.
* Add a new DocumentContextExecutor component for executing a Callable
  with a document in context.
* Add tests.


Vulnerable code:
    <xwiki.jacoco.instructionRatio>0.39</xwiki.jacoco.instructionRatio>
        return velocityRenderer.render(contentToParse.toString());
            throw new IconException("The icon set is null", null);
        String url = velocityRenderer.render(iconSet.getCss());
        Map<String, Object> parameters = new HashMap();
        linkExtension.use(url, parameters);
        skinExtension.use(iconSet.getSsx());
        jsExtension.use(iconSet.getJsx());
            DocumentModelBridge doc = documentAccessBridge.getDocumentInstance(iconSetReference);
            DocumentReference iconClassRef = new DocumentReference(wikiDescriptorManager.getCurrentWikiId(),
            String name = (String) documentAccessBridge.getProperty(iconSetReference, iconClassRef, "name");
            return loadIconSet(new StringReader(content), name);
        IconSet iconSet = iconSetCache.get(name, wikiDescriptorManager.getCurrentWikiId());
                Query query = queryManager.createQuery(xwql, Query.XWQL);
                List<String> results = query.execute();
                if (results.isEmpty()) {
                    return null;
                }
                // Get the first result
                String docName = results.get(0);
                DocumentReference docRef = documentReferenceResolver.resolve(docName);
                iconSet = iconSetLoader.loadIconSet(docRef);
                iconSetCache.put(docRef, iconSet);
                iconSetCache.put(name, wikiDescriptorManager.getCurrentWikiId(), iconSet);
            } catch (QueryException e) {
                throw new IconException(String.format("Failed to load the icon set [%s].", name), e);
        // Return the icon set
import org.xwiki.velocity.XWikiVelocityException;
    public String render(String code) throws IconException
            engine = velocityManager.getVelocityEngine();
            VelocityContext context = new XWikiVelocityContext(velocityManager.getVelocityContext(),
            if (engine.evaluate(context, output, "DefaultIconRenderer", code)) {
                return output.toString();
            } else {
                // I don't know how to check the velocity runtime log
                throw new IconException("Failed to render the icon. See the Velocity runtime log.", null);
        } catch (XWikiVelocityException e) {
        when(velocityRenderer.render("#set($icon = \"blabla\")\nimage:$icon.png")).thenReturn("image:blabla.png");
        when(velocityRenderer.render("css")).thenReturn("velocityParsedCSS");
        when(velocityRenderer.render("#set($icon = \"blabla\")\n<img src=\"$icon.png\" />"))
        when(velocityRenderer.render("css")).thenReturn("velocityParsedCSS");
        IconException exception = new IconException("exception", null);
        when(velocityRenderer.render(any())).thenThrow(exception);
        when(velocityRenderer.render("#set($icon = \"hello\")\nfa fa-$icon")).thenReturn("fa fa-hello");
import static org.mockito.ArgumentMatchers.eq;
public class DefaultIconSetLoaderTest
    public void setUp() throws Exception
        when(wikiDescriptorManager.getCurrentWikiId()).thenReturn("wikiId");
    public void loadIconSet() throws Exception
        IconSet result = iconSetLoader.loadIconSet(content, "FontAwesome");
    public void loadIconSetFromWikiDocument() throws Exception
        when(documentAccessBridge.getProperty(eq(iconSetRef), eq(iconClassRef), eq("name"))).thenReturn("MyIconTheme");
        when(documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc);
        IconSet result = iconSetLoader.loadIconSet(iconSetRef);
    public void loadIconSetWithException() throws Exception
        Exception caughException = null;
        try {
            iconSetLoader.loadIconSet(content, "FontAwesome");
        } catch (IconException e) {
            caughException = e;
        }

        assertNotNull(caughException);
        assert (caughException instanceof IconException);
        assertEquals(exception, caughException.getCause());
        assertEquals("Failed to load the IconSet [FontAwesome].", caughException.getMessage());
    public void loadIconSetFromWikiDocumentWithException() throws Exception
        when(documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception);
        // Test
        Exception caughException = null;
        try {
            iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c"));
        } catch (IconException e) {
            caughException = e;
        }

        assertNotNull(caughException);
        assert (caughException instanceof IconException);
        assertEquals(exception, caughException.getCause());
        assertEquals("Failed to load the IconSet [a:b.c].", caughException.getMessage());
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.xwiki.test.mockito.MockitoComponentMockingRule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class VelocityRendererTest
    @Rule
    public MockitoComponentMockingRule<VelocityRenderer> mocker =
            new MockitoComponentMockingRule<>(VelocityRenderer.class);

    @Before
    public void setUp() throws Exception
    {
        velocityManager = mocker.getInstance(VelocityManager.class);
    }
    public void renderTest() throws Exception
        when(velocityManager.getVelocityEngine()).thenReturn(engine);
                new Answer<Object>()
                {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable
                    {
                        // Get the writer
                        Writer writer = (Writer) invocation.getArguments()[1];
                        writer.write("Rendered code");
                        return true;
                    }
                });
        assertEquals("Rendered code", mocker.getComponentUnderTest().render("myCode"));
    public void renderWithException() throws Exception
        when(velocityManager.getVelocityEngine()).thenThrow(exception);
            mocker.getComponentUnderTest().render("myCode");
        } catch(IconException e) {
    public void renderWhenEvaluateReturnsFalse() throws Exception
        when(velocityManager.getVelocityEngine()).thenReturn(engine);
        IconException caughtException = null;
        try {
            mocker.getComponentUnderTest().render("myCode");
        } catch(IconException e) {
            caughtException = e;
        }
        assertNotNull(caughtException);
        assertEquals("Failed to render the icon. See the Velocity runtime log.", caughtException.getMessage());
        IconException iconException = new IconException("icon error", null);
        if (content == null || syntax == null) {
            return null;
        }

        // Determine the escape character for the syntax.
        char escapeChar;
        try {
            escapeChar = getEscapeCharacter(syntax);
        } catch (Exception e) {
            // We don`t know how to proceed, so we just return null.
            return null;
        }

        // Since we prefix all characters, the result size will be double the input's, so we can just use char[].
        char[] result = new char[content.length() * 2];

        // Escape the content.
        for (int i = 0; i < content.length(); i++) {
            result[2 * i] = escapeChar;
            result[2 * i + 1] = content.charAt(i);
        }

        return String.valueOf(result);

    private char getEscapeCharacter(Syntax syntax) throws IllegalArgumentException
    {
        if (Syntax.XWIKI_1_0.equals(syntax)) {
            return '\\';
        } else if (Syntax.XWIKI_2_0.equals(syntax) || Syntax.XWIKI_2_1.equals(syntax)) {
            return '~';
        }

        throw new IllegalArgumentException(String.format("Escaping is not supported for Syntax [%s]", syntax));
    }
@ComponentList({ ContextComponentManagerProvider.class })

Fixed code:
    <xwiki.jacoco.instructionRatio>0.50</xwiki.jacoco.instructionRatio>
/*
 * 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.bridge.internal;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.ModelContext;
import org.xwiki.model.reference.EntityReference;

/**
 * Default implementation of {@link DocumentContextExecutor}.
 *
 * @version $Id$
 * @since 14.10.6
 * @since 15.2RC1
 */
@Component
@Singleton
public class DefaultDocumentContextExecutor implements DocumentContextExecutor
{
    @Inject
    private DocumentAccessBridge documentAccessBridge;

    @Inject
    private ModelContext modelContext;

    @Override
    public <V> V call(Callable<V> callable, DocumentModelBridge document) throws Exception
    {
        Map<String, Object> backupObjects = new HashMap<>();
        EntityReference currentWikiReference = this.modelContext.getCurrentEntityReference();
        boolean canPop = false;

        try {
            this.documentAccessBridge.pushDocumentInContext(backupObjects, document);
            canPop = true;
            // Make sure to synchronize the context wiki with the context document's wiki.
            this.modelContext.setCurrentEntityReference(document.getDocumentReference().getWikiReference());

            return callable.call();
        } finally {
            if (canPop) {
                this.documentAccessBridge.popDocumentFromContext(backupObjects);
                // Also restore the context wiki.
                this.modelContext.setCurrentEntityReference(currentWikiReference);
            }
        }
    }
}
/*
 * 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.bridge.internal;

import java.util.concurrent.Callable;

import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.component.annotation.Role;

/**
 * Executes a {@link Callable} with the given document in context.
 *
 * @version $Id$
 * @since 14.10.6
 * @since 15.2RC1
 */
@Role
public interface DocumentContextExecutor
{
    /**
     * Execute the passed {@link Callable} with the given document in context.
     *
     * @param callable the task to execute
     * @param document the document to put in context
     * @return computed result
     * @param <V> the result type of method {@code call}
     */
    <V> V call(Callable<V> callable, DocumentModelBridge document) throws Exception;
}
org.xwiki.bridge.internal.DefaultDocumentContextExecutor
/*
 * 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.bridge.internal;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.model.EntityType;
import org.xwiki.model.ModelContext;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.WikiReference;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;

import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * Unit test for {@link DefaultDocumentContextExecutor}.
 *
 * @version $Id$
 */
@ComponentTest
class DefaultDocumentContextExecutorTest
{
    private static final WikiReference TEST_WIKI_REFERENCE = new WikiReference("xwiki");

    private static final DocumentReference TEST_DOCUMENT_REFERENCE =
        new DocumentReference(TEST_WIKI_REFERENCE.getName(), "Space", "WebHome");

    private static final WikiReference INITIAL_WIKI_REFERENCE = new WikiReference("initial");

    @MockComponent
    private ModelContext modelContext;

    @MockComponent
    private DocumentAccessBridge documentAccessBridge;

    @InjectMockComponents
    private DefaultDocumentContextExecutor executor;

    @Mock
    private Callable<Object> mockCallable;

    @Mock
    private DocumentModelBridge documentModelBridge;

    private WikiReference modelWikiReference;

    @BeforeEach
    public void beforeEach()
    {
        when(this.documentModelBridge.getDocumentReference()).thenReturn(TEST_DOCUMENT_REFERENCE);

        // Mock actually storing the wiki reference
        this.modelWikiReference = INITIAL_WIKI_REFERENCE;
        when(this.modelContext.getCurrentEntityReference()).thenReturn(this.modelWikiReference);
        doAnswer(invocationOnMock -> {
            EntityReference entityWikiReference =
                invocationOnMock.getArgument(0, EntityReference.class).extractReference(EntityType.WIKI);
            this.modelWikiReference = new WikiReference(entityWikiReference.getName());
            return null;
        }).when(this.modelContext).setCurrentEntityReference(any(EntityReference.class));
    }

    @Test
    void call() throws Exception
    {
        String callResult = "callResult";
        when(this.mockCallable.call()).thenReturn(callResult);
        AtomicReference<Map<String, Object>> backupObjects = new AtomicReference<>();
        doAnswer(invocationOnMock -> {
            backupObjects.set(invocationOnMock.getArgument(0));
            return null;
        }).when(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));

        Object actualResult = this.executor.call(this.mockCallable, this.documentModelBridge);

        assertSame(callResult, actualResult);
        InOrder inOrder = inOrder(this.documentAccessBridge, this.modelContext);
        inOrder.verify(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));
        inOrder.verify(this.modelContext).setCurrentEntityReference(TEST_WIKI_REFERENCE);
        inOrder.verify(this.documentAccessBridge).popDocumentFromContext(backupObjects.get());
        inOrder.verify(this.modelContext).setCurrentEntityReference(INITIAL_WIKI_REFERENCE);
    }

    @Test
    void popWhenCallableThrows() throws Exception
    {
        Exception testException = new Exception("Callable failed");
        when(this.mockCallable.call()).thenThrow(testException);

        assertThrows(Exception.class, () -> this.executor.call(this.mockCallable, this.documentModelBridge));
        InOrder inOrder = inOrder(this.documentAccessBridge, this.modelContext);
        inOrder.verify(this.modelContext).setCurrentEntityReference(TEST_WIKI_REFERENCE);
        inOrder.verify(this.documentAccessBridge).popDocumentFromContext(any());
        inOrder.verify(this.modelContext).setCurrentEntityReference(INITIAL_WIKI_REFERENCE);
    }

    @Test
    void doNothingWhenPushThrows() throws Exception
    {
        Exception testException = new Exception("Test");
        doThrow(testException)
            .when(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));

        assertThrows(Exception.class, () -> this.executor.call(this.mockCallable, this.documentModelBridge));

        verifyNoInteractions(this.mockCallable);
        verify(this.modelContext, never()).setCurrentEntityReference(any());
    }
}

    /**
     * Constructor with just a message.
     *
     * @param message the message to store in the exception
     * @since 14.10.6
     * @since 15.2RC1
     */
    public IconException(String message)
    {
        super(message);
    }
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.stability.Unstable;

    private DocumentReference sourceDocumentReference;


    /**
     * @return the document reference of the source of the icon theme, may be {@code null} if the icon set hasn't
     * been loaded from a document
     * @since 14.10.6
     * @since 15.2RC1
     */
    @Unstable
    public DocumentReference getSourceDocumentReference()
    {
        return this.sourceDocumentReference;
    }

    /**
     * @param sourceDocumentReference the reference to the source of the icon theme
     * @since 14.10.6
     * @since 15.2RC1
     */
    @Unstable
    public void setSourceDocumentReference(DocumentReference sourceDocumentReference)
    {
        this.sourceDocumentReference = sourceDocumentReference;
    }
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-user-api</artifactId>
      <version>${project.version}</version>
    </dependency>
        return this.velocityRenderer.render(contentToParse.toString(), iconSet.getSourceDocumentReference());
            throw new IconException("The icon set is null");
        String url = this.velocityRenderer.render(iconSet.getCss(), iconSet.getSourceDocumentReference());
        Map<String, Object> parameters = new HashMap<>();
        this.linkExtension.use(url, parameters);
        this.skinExtension.use(iconSet.getSsx());
        this.jsExtension.use(iconSet.getJsx());
import javax.inject.Named;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.user.UserReferenceSerializer;
    @Inject
    private AuthorizationManager authorizationManager;

    @Inject
    @Named("document")
    private UserReferenceSerializer<DocumentReference> documentUserSerializer;

            DocumentModelBridge doc = this.documentAccessBridge.getDocumentInstance(iconSetReference);

            // Check that both the content (actual icon theme content) and the metadata author (icon theme object)
            // have script right.
            DocumentReference contentAuthor =
                this.documentUserSerializer.serialize(doc.getAuthors().getContentAuthor());
            this.authorizationManager.checkAccess(Right.SCRIPT, contentAuthor, iconSetReference);
            DocumentReference metadataAuthor =
                this.documentUserSerializer.serialize(doc.getAuthors().getEffectiveMetadataAuthor());
            this.authorizationManager.checkAccess(Right.SCRIPT, metadataAuthor, iconSetReference);

            DocumentReference iconClassRef = new DocumentReference(this.wikiDescriptorManager.getCurrentWikiId(),
            String name = (String) this.documentAccessBridge.getProperty(iconSetReference, iconClassRef, "name");
            IconSet result = loadIconSet(new StringReader(content), name);
            result.setSourceDocumentReference(iconSetReference);
            return result;
import java.util.ArrayList;
import org.slf4j.Logger;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;

    @Inject
    private Logger logger;

        IconSet iconSet = this.iconSetCache.get(name, this.wikiDescriptorManager.getCurrentWikiId());
            List<String> results;

                Query query = this.queryManager.createQuery(xwql, Query.XWQL);
                results = query.execute();
            } catch (QueryException e) {
                throw new IconException(String.format("Failed to load the icon set [%s].", name), e);
            }

            iconSet = loadIconSetFromCandidateDocuments(name, results);
        }

        // Return the icon set
        return iconSet;
    }

    private IconSet loadIconSetFromCandidateDocuments(String name, List<String> candidateDocuments) throws IconException
    {
        List<IconException> iconExceptions = new ArrayList<>();
        IconSet iconSet = null;
        // Try all results to find the first one that loads successfully.
        for (String docName : candidateDocuments) {
            DocumentReference docRef = this.documentReferenceResolver.resolve(docName);
            try {
                iconSet = this.iconSetLoader.loadIconSet(docRef);
                this.iconSetCache.put(docRef, iconSet);
                this.iconSetCache.put(name, this.wikiDescriptorManager.getCurrentWikiId(), iconSet);

                break;
            } catch (IconException e) {
                // Store the exception first, maybe there is another icon theme with the same name that loads
                // successfully.
                iconExceptions.add(e);
            }
        }

        if (iconSet == null && !iconExceptions.isEmpty()) {
            if (iconExceptions.size() > 1) {
                iconExceptions.stream().skip(1)
                    .forEach(e -> this.logger.warn("Failed loading icon set [{}] from multiple matching "
                            + "documents, ignored this additional exception, reason: [{}].", name,
                        getRootCauseMessage(e)));
                throw new IconException(String.format("Failed to load the icon set [%s] from %d documents, "
                        + "reporting the first exception, see the log for additional errors.",
                    name, candidateDocuments.size()),
                    iconExceptions.get(0));
            } else {
                throw iconExceptions.get(0);
import java.util.concurrent.Callable;
import javax.inject.Named;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.bridge.internal.DocumentContextExecutor;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.user.UserReferenceSerializer;
    private static final String NAMESPACE = "DefaultIconRenderer";

    @Inject
    private DocumentAccessBridge documentAccessBridge;

    @Inject
    private AuthorExecutor authorExecutor;

    @Inject
    private DocumentContextExecutor documentContextExecutor;

    @Inject
    @Named("document")
    private UserReferenceSerializer<DocumentReference> documentUserSerializer;

     * @param contextDocumentReference the reference of the context document
    public String render(String code, DocumentReference contextDocumentReference) throws IconException

        boolean result;

            engine = this.velocityManager.getVelocityEngine();
            DocumentReference authorReference;
            DocumentModelBridge sourceDocument;

            // Execute the Velocity code in an isolated execution context with the rights of its author when the icon
            // theme is from a document.
            if (contextDocumentReference != null) {
                sourceDocument =
                    this.documentAccessBridge.getDocumentInstance(contextDocumentReference);
                authorReference = this.documentUserSerializer.serialize(sourceDocument.getAuthors().getContentAuthor());
            } else {
                authorReference = null;
                sourceDocument = null;
            }

            VelocityContext context = new XWikiVelocityContext(this.velocityManager.getVelocityContext(),
            VelocityEngine finalEngine = engine;
            Callable<Boolean> callable = () -> finalEngine.evaluate(context, output, NAMESPACE, code);
            if (contextDocumentReference != null) {
                // Wrap the callable in a document context and author executor to ensure that the document is in
                // context and the Velocity code is executed with the author's rights.
                Callable<Boolean> innerCallable = callable;
                callable = () -> this.documentContextExecutor.call(
                    () -> this.authorExecutor.call(innerCallable, authorReference, contextDocumentReference),
                    sourceDocument);
            result = callable.call();
        } catch (Exception e) {

        if (result) {
            return output.toString();
        } else {
            // I don't know how to check the velocity runtime log
            throw new IconException("Failed to render the icon. See the Velocity runtime log.");
        }
        when(this.velocityRenderer.render("#set($icon = \"blabla\")\nimage:$icon.png", null))
            .thenReturn("image:blabla.png");
        when(this.velocityRenderer.render("css", null)).thenReturn("velocityParsedCSS");
        when(this.velocityRenderer.render("#set($icon = \"blabla\")\n<img src=\"$icon.png\" />", null))
        when(this.velocityRenderer.render("css", null)).thenReturn("velocityParsedCSS");
        IconException exception = new IconException("exception");
        when(this.velocityRenderer.render(any(), any())).thenThrow(exception);
        when(this.velocityRenderer.render("#set($icon = \"hello\")\nfa fa-$icon", null)).thenReturn("fa fa-hello");
import org.xwiki.model.internal.document.DefaultDocumentAuthors;
import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertThrows;
class DefaultIconSetLoaderTest
    void setUp()
        when(this.wikiDescriptorManager.getCurrentWikiId()).thenReturn("wikiId");
    void loadIconSet() throws Exception
        IconSet result = this.iconSetLoader.loadIconSet(content, "FontAwesome");
    void loadIconSetFromWikiDocument() throws Exception
        when(this.documentAccessBridge.getProperty(iconSetRef, iconClassRef, "name")).thenReturn("MyIconTheme");
        when(this.documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc);
        when(doc.getAuthors()).thenReturn(new DefaultDocumentAuthors(new XWikiDocument(iconSetRef)));

        IconSet result = this.iconSetLoader.loadIconSet(iconSetRef);
    void loadIconSetWithException() throws Exception
        Exception caughtException = assertThrows(IconException.class, () ->
            this.iconSetLoader.loadIconSet(content, "FontAwesome"));

        assertEquals(exception, caughtException.getCause());
        assertEquals("Failed to load the IconSet [FontAwesome].", caughtException.getMessage());
    void loadIconSetFromWikiDocumentWithException() throws Exception
        when(this.documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception);
        IconException caughtException = assertThrows(IconException.class, () ->
            this.iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c")));

        assertEquals(exception, caughtException.getCause());
        assertEquals("Failed to load the IconSet [a:b.c].", caughtException.getMessage());
import org.junit.jupiter.api.extension.RegisterExtension;
import org.xwiki.test.LogLevel;
import org.xwiki.test.junit5.LogCaptureExtension;
import static org.junit.jupiter.api.Assertions.assertThrows;
    @RegisterExtension
    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

    @Test
    void getIconSetWhenOneFails() throws Exception
    {
        // Mocks
        IconSet iconSet = new IconSet("silk");
        Query query = mock(Query.class);
        when(this.queryManager.createQuery("FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name",
            Query.XWQL)).thenReturn(query);
        List<String> results = List.of("FakeIcon.Silk", "IconThemes.Silk");
        when(query.<String>execute()).thenReturn(results);
        DocumentReference fakeDocumentReference = new DocumentReference("wiki", "FakeIcon", "Silk");
        when(this.documentReferenceResolver.resolve("FakeIcon.Silk")).thenReturn(fakeDocumentReference);
        when(this.iconSetLoader.loadIconSet(fakeDocumentReference)).thenThrow(new IconException("Test"));

        DocumentReference documentReference = new DocumentReference("wiki", "IconThemes", "Silk");
        when(this.documentReferenceResolver.resolve("IconThemes.Silk")).thenReturn(documentReference);
        when(this.iconSetLoader.loadIconSet(documentReference)).thenReturn(iconSet);

        // Test
        assertEquals(iconSet, this.iconSetManager.getIconSet("silk"));

        // Verify
        verify(query).bindValue("name", "silk");
        verify(this.iconSetCache).put(documentReference, iconSet);
        verify(this.iconSetCache).put("silk", "currentWikiId", iconSet);
        verify(this.iconSetLoader).loadIconSet(fakeDocumentReference);
    }

    @Test
    void getIconSetWhenAllFail() throws Exception
    {
        // Mocks
        Query query = mock(Query.class);
        when(this.queryManager.createQuery("FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name",
            Query.XWQL)).thenReturn(query);
        List<String> results = List.of("FakeIcon.Silk", "IconThemes.Silk");
        when(query.<String>execute()).thenReturn(results);
        DocumentReference fakeDocumentReference = new DocumentReference("wiki", "FakeIcon", "Silk");
        when(this.documentReferenceResolver.resolve("FakeIcon.Silk")).thenReturn(fakeDocumentReference);
        IconException fakeException = new IconException("Fake");
        when(this.iconSetLoader.loadIconSet(fakeDocumentReference)).thenThrow(fakeException);

        DocumentReference documentReference = new DocumentReference("wiki", "IconThemes", "Silk");
        when(this.documentReferenceResolver.resolve("IconThemes.Silk")).thenReturn(documentReference);
        when(this.iconSetLoader.loadIconSet(documentReference)).thenThrow(new IconException("Real"));

        // Test
        IconException exception = assertThrows(IconException.class, () -> this.iconSetManager.getIconSet("silk"));
        assertEquals("Failed to load the icon set [silk] from 2 documents, reporting the first exception, see the"
            + " log for additional errors.", exception.getMessage());
        assertEquals(fakeException, exception.getCause());

        assertEquals(1, this.logCapture.size());
        assertEquals("Failed loading icon set [silk] from multiple matching documents, "
                + "ignored this additional exception, reason: [IconException: Real].",
            this.logCapture.getMessage(0));

        // Verify
        verify(query).bindValue("name", "silk");
        verify(this.iconSetCache, never()).put(anyString(), any());
        verify(this.iconSetCache, never()).put(any(DocumentReference.class), any());
        verify(this.iconSetLoader).loadIconSet(fakeDocumentReference);
    }

import java.util.concurrent.Callable;

import javax.inject.Named;
import org.junit.jupiter.api.Test;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.internal.DocumentContextExecutor;
import org.xwiki.model.document.DocumentAuthors;
import org.xwiki.model.internal.document.DefaultDocumentAuthors;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceSerializer;
import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ComponentTest
class VelocityRendererTest
    @MockComponent
    @MockComponent
    private DocumentAccessBridge documentAccessBridge;

    @MockComponent
    @Named("document")
    private UserReferenceSerializer<DocumentReference> documentUserSerializer;

    @MockComponent
    private AuthorExecutor authorExecutor;

    @MockComponent
    private DocumentContextExecutor documentContextExecutor;

    @InjectMockComponents
    private VelocityRenderer velocityRenderer;
    void renderTest() throws Exception
        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
            invocation -> {
                // Get the writer
                Writer writer = (Writer) invocation.getArguments()[1];
                writer.write("Rendered code");
                return true;
            });
        assertEquals("Rendered code", this.velocityRenderer.render("myCode", null));
    void renderWithException() throws Exception
        when(this.velocityManager.getVelocityEngine()).thenThrow(exception);
            this.velocityRenderer.render("myCode", null);
        } catch (IconException e) {
    void renderWhenEvaluateReturnsFalse() throws Exception
        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
        IconException caughtException = assertThrows(IconException.class,
            () -> this.velocityRenderer.render("myCode", null));
        assertEquals("Failed to render the icon. See the Velocity runtime log.",
            caughtException.getMessage());
    @Test
    void renderWithContextDocument() throws Exception
    {
        // Mocks
        VelocityEngine engine = mock(VelocityEngine.class);
        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
        when(engine.evaluate(any(VelocityContext.class), any(Writer.class), any(), eq("myCode"))).thenAnswer(
            invocation -> {
                // Get the writer
                Writer writer = (Writer) invocation.getArguments()[1];
                writer.write("Rendered code");
                return true;
            });

        DocumentReference contextReference = new DocumentReference("xwiki", "Space", "IconTheme");
        DocumentReference documentAuthorReference = new DocumentReference("xwiki", "XWiki", "User");
        XWikiDocument document = mock(XWikiDocument.class);
        UserReference authorReference = mock(UserReference.class);
        DocumentAuthors documentAuthors = new DefaultDocumentAuthors(document);
        documentAuthors.setContentAuthor(authorReference);
        when(document.getAuthors()).thenReturn(documentAuthors);
        when(this.documentUserSerializer.serialize(authorReference)).thenReturn(documentAuthorReference);
        when(this.documentAccessBridge.getDocumentInstance(contextReference)).thenReturn(document);
        when(this.authorExecutor.call(any(), eq(documentAuthorReference), eq(contextReference)))
            .then(invocation -> invocation.getArgument(0, Callable.class).call());
        when(this.documentContextExecutor.call(any(), eq(document)))
            .then(invocation -> invocation.getArgument(0, Callable.class).call());

        assertEquals("Rendered code", this.velocityRenderer.render("myCode", contextReference));
    }
        IconException iconException = new IconException("icon 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.rendering.internal.util;

import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.rendering.syntax.Syntax;

/**
 * Escaping helpers.
 *
 * @version $Id$
 * @since 14.10.6
 * @since 15.2RC1
 */
@Component(roles = XWikiSyntaxEscaper.class)
@Singleton
public class XWikiSyntaxEscaper
{
    /**
     * Escapes a give text using the escaping method specific to the given syntax.
     * <p>
     * One example of escaping method is using escape characters like {@code ~} for the {@link Syntax#XWIKI_2_1} syntax
     * on all or just some characters of the given text.
     * <p>
     * The current implementation only escapes XWiki 1.0, 2.0 and 2.1 syntaxes.
     *
     * @param content the text to escape
     * @param syntax the syntax to escape the content in (e.g. {@link Syntax#XWIKI_1_0}, {@link Syntax#XWIKI_2_0},
     *     {@link Syntax#XWIKI_2_1}, etc.). This is the syntax where the output will be used and not necessarily the
     *     same syntax of the input content
     * @return the escaped text or {@code null} if the given content or the given syntax are {@code null}, or if the
     *     syntax is not supported
     */
    public String escape(String content, Syntax syntax)
    {
        if (content == null || syntax == null) {
            return null;
        }

        // Determine the escape character for the syntax.
        char escapeChar;
        try {
            escapeChar = getEscapeCharacter(syntax);
        } catch (Exception e) {
            // We don`t know how to proceed, so we just return null.
            return null;
        }

        // Since we prefix all characters, the result size will be double the input's, so we can just use char[].
        char[] result = new char[content.length() * 2];

        // Escape the content.
        for (int i = 0; i < content.length(); i++) {
            result[2 * i] = escapeChar;
            result[2 * i + 1] = content.charAt(i);
        }

        return String.valueOf(result);
    }

    private char getEscapeCharacter(Syntax syntax) throws IllegalArgumentException
    {
        if (Syntax.XWIKI_1_0.equals(syntax)) {
            return '\\';
        } else if (Syntax.XWIKI_2_0.equals(syntax) || Syntax.XWIKI_2_1.equals(syntax)) {
            return '~';
        }

        throw new IllegalArgumentException(String.format("Escaping is not supported for Syntax [%s]", syntax));
    }
}
import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
    @Inject
    private XWikiSyntaxEscaper escaper;

        return this.escaper.escape(content, syntax);
org.xwiki.rendering.internal.util.XWikiSyntaxEscaper
import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
@ComponentList({ ContextComponentManagerProvider.class, XWikiSyntaxEscaper.class })