CVE-2024-31981: Insufficient Information (NVD-CWE-noinfo)
value: XWiki Platform is a generic wiki platform. Starting in version 3.0.1 and prior to versions 4.10.20, 15.5.4, and 15.10-rc-1, remote code execution is possible via PDF export templates. This vulnerability has been patched in XWiki 14.10.20, 15.5.4 and 15.10-rc-1. If PDF templates are not typically used on the instance, an administrator can create the document `XWiki.PDFClass` and block its edition, after making sure that it does not contain a `style` attribute. Otherwise, there are no known workarounds aside from upgrading.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-10T20:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/pdf/impl/PdfExportImpl.java, xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/pdf/impl/PdfExportImplTest.java
Commit date: 2023-11-16 16:25:01 +0100
Commit message: XWIKI-21337: Apply PDF templates with the rights of their authors

Diff: diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/pdf/impl/PdfExportImpl.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/pdf/impl/PdfExportImpl.java
index 9f471406bc61f..b4d86b793d545 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/pdf/impl/PdfExportImpl.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/pdf/impl/PdfExportImpl.java
@@ -27,21 +27,22 @@
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.lang.reflect.Type;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 
 import javax.xml.transform.sax.SAXSource;
 
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.velocity.VelocityContext;
 import org.dom4j.Element;
 import org.dom4j.io.OutputFormat;
 import org.dom4j.io.SAXReader;
 import org.dom4j.io.XMLWriter;
@@ -52,12 +53,15 @@
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.environment.Environment;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.DocumentReferenceResolver;
 import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.user.UserReferenceSerializer;
 import org.xwiki.velocity.VelocityManager;
-import org.xwiki.velocity.XWikiVelocityException;
 import org.xwiki.xml.EntityResolver;
 import org.xwiki.xml.XMLReaderFactory;
 import org.xwiki.xml.XMLUtils;
 import org.xwiki.xml.html.HTMLCleaner;
 import org.xwiki.xml.html.HTMLCleanerConfiguration;
@@ -93,34 +97,41 @@ public class PdfExportImpl implements PdfExport
 
     /** Logging helper object. */
     private static final Logger LOGGER = LoggerFactory.getLogger(PdfExportImpl.class);
 
     /** Document name resolver. */
-    private static DocumentReferenceResolver<String> referenceResolver =
+    private final DocumentReferenceResolver<String> referenceResolver =
         Utils.getComponent(DocumentReferenceResolver.TYPE_STRING, "currentmixed");
 
     /** Document name serializer. */
-    private static EntityReferenceSerializer<String> referenceSerializer =
+    private final EntityReferenceSerializer<String> referenceSerializer =
         Utils.getComponent(EntityReferenceSerializer.TYPE_STRING);
 
     /** Provides access to document properties. */
-    private static DocumentAccessBridge dab = Utils.getComponent(DocumentAccessBridge.class);
+    private final DocumentAccessBridge dab = Utils.getComponent(DocumentAccessBridge.class);
 
     /** Velocity engine manager, used for interpreting velocity. */
-    private static VelocityManager velocityManager = Utils.getComponent(VelocityManager.class);
+    private final VelocityManager velocityManager = Utils.getComponent(VelocityManager.class);
 
-    private static XMLReaderFactory xmlReaderFactory = Utils.getComponent(XMLReaderFactory.class);
+    private final XMLReaderFactory xmlReaderFactory = Utils.getComponent(XMLReaderFactory.class);
+
+    private final AuthorizationManager authorizationManager = Utils.getComponent(AuthorizationManager.class);
+
+    private final AuthorExecutor authorExecutor = Utils.getComponent(AuthorExecutor.class);
+
+    private final UserReferenceSerializer<DocumentReference> userReferenceSerializer =
+        Utils.getComponent(UserReferenceSerializer.TYPE_DOCUMENT_REFERENCE, "document");
 
     /**
      * Used to get the temporary directory.
      */
-    private Environment environment = Utils.getComponent((Type) Environment.class);
+    private final Environment environment = Utils.getComponent((Type) Environment.class);
 
     /**
      * Used to render XSL-FO to PDF.
      */
-    private XSLFORenderer xslFORenderer = Utils.getComponent(XSLFORenderer.class, "fop");
+    private final XSLFORenderer xslFORenderer = Utils.getComponent(XSLFORenderer.class, "fop");
 
     @Override
     public void exportToPDF(XWikiDocument doc, OutputStream out, XWikiContext context) throws XWikiException
     {
         export(doc, out, ExportType.PDF, context);
@@ -182,11 +193,11 @@ private String convertToStrictXHtml(String input)
     {
         LOGGER.debug("Cleaning HTML:\n{}", input);
 
         HTMLCleaner cleaner = Utils.getComponent(HTMLCleaner.class);
         HTMLCleanerConfiguration config = cleaner.getDefaultConfiguration();
-        List<HTMLFilter> filters = new ArrayList<HTMLFilter>(config.getFilters());
+        List<HTMLFilter> filters = new ArrayList<>(config.getFilters());
         filters.add(Utils.getComponent(HTMLFilter.class, "uniqueId"));
         config.setFilters(filters);
         String result = HTMLUtils.toString(cleaner.clean(new StringReader(input), config));
         LOGGER.debug("Cleaned XHTML:\n{}", result);
         return result;
@@ -259,11 +270,11 @@ private String convertXHtmlToXMLFO(String xhtml, XWikiContext context) throws XW
      */
     private void renderXSLFO(String xmlfo, OutputStream out, ExportType type, final XWikiContext context)
         throws XWikiException
     {
         try {
-            this.xslFORenderer.render(new ByteArrayInputStream(xmlfo.getBytes("UTF-8")), out, type.getMimeType());
+            this.xslFORenderer.render(new ByteArrayInputStream(xmlfo.getBytes(StandardCharsets.UTF_8)), out, type.getMimeType());
         } catch (IllegalStateException e) {
             throw createException(e, type, XWikiException.ERROR_XWIKI_APP_SEND_RESPONSE_EXCEPTION);
         } catch (Exception e) {
             throw createException(e, type, XWikiException.ERROR_XWIKI_EXPORT_PDF_FOP_FAILED);
         }
@@ -336,11 +347,11 @@ String applyCSS(String html, String css, XWikiContext context)
             XHTMLDocumentFactory docFactory = XHTMLDocumentFactory.getInstance();
             SAXReader reader = new SAXReader(docFactory);
 
             // Dom4J 2.1.1 disables external DTDs by default, so we set our own XMLReader.
             // See https://github.com/dom4j/dom4j/issues/51
-            XMLReader xmlReader = xmlReaderFactory.createXMLReader();
+            XMLReader xmlReader = this.xmlReaderFactory.createXMLReader();
             reader.setXMLReader(xmlReader);
 
             reader.setEntityResolver(new DefaultEntityResolver());
             XHTMLDocument document = (XHTMLDocument) reader.read(source);
 
@@ -463,32 +474,51 @@ private String getPDFTemplateProperty(String propertyName, XWikiContext context)
         String pdftemplate = context.getRequest().getParameter("pdftemplate");
 
         DocumentReference templateReference;
         DocumentReference classReference;
         if (StringUtils.isNotEmpty(pdftemplate)) {
-            templateReference = referenceResolver.resolve(pdftemplate);
+            templateReference = this.referenceResolver.resolve(pdftemplate);
             classReference = new DocumentReference(templateReference.getWikiReference().getName(), "XWiki", "PDFClass");
         } else {
-            templateReference = dab.getCurrentDocumentReference();
-            String currentWiki = dab.getCurrentDocumentReference().getRoot().getName();
+            templateReference = this.dab.getCurrentDocumentReference();
+            String currentWiki = this.dab.getCurrentDocumentReference().getRoot().getName();
             classReference = new DocumentReference(currentWiki, "XWiki", "PDFClass");
         }
 
-        String result = (String) dab.getProperty(templateReference, classReference, propertyName);
-        if (StringUtils.isBlank(result)) {
+        String templateContent = (String) this.dab.getProperty(templateReference, classReference, propertyName);
+        if (StringUtils.isBlank(templateContent)) {
             return "";
         }
-        String templateName = referenceSerializer.serialize(templateReference);
+
+        String templateName = this.referenceSerializer.serialize(templateReference);
+        DocumentReference templateAuthorReference;
+        String result = templateContent;
         try {
-            StringWriter writer = new StringWriter();
-            VelocityContext vcontext = velocityManager.getVelocityContext();
-            velocityManager.getVelocityEngine().evaluate(vcontext, writer, templateName, result);
-            result = writer.toString();
-        } catch (XWikiVelocityException e) {
-            LOGGER.warn("Error applying Velocity to the [{}] property of the [{}] document. Using the property's value "
-                + "without applying Velocity.", propertyName, templateName, ExceptionUtils.getRootCauseMessage(e));
+             templateAuthorReference = this.userReferenceSerializer.serialize(
+                 this.dab.getDocumentInstance(templateReference).getAuthors().getEffectiveMetadataAuthor());
+        } catch (Exception e) {
+            LOGGER.warn("Error fetching the author of template [{}] during PDF conversion. Using the [{}] property of "
+                + "the document's value without applying Velocity.", templateName, propertyName);
+            return result;
         }
+
+        if (this.authorizationManager.hasAccess(Right.SCRIPT, templateAuthorReference, templateReference)) {
+            try {
+                result = this.authorExecutor.call(() -> {
+                    StringWriter writer = new StringWriter();
+                    VelocityContext vcontext = this.velocityManager.getVelocityContext();
+                    this.velocityManager.getVelocityEngine().evaluate(vcontext, writer, templateName,
+                        templateContent);
+                    return writer.toString();
+                }, templateAuthorReference, templateReference);
+            } catch (Exception e) {
+                LOGGER.warn("Failed to run Velocity engine in author executor. Using the [{}] property of the [{}] "
+                    + "document's value without applying Velocity. Reason: [{}]",
+                    propertyName, templateName, ExceptionUtils.getRootCauseMessage(e));
+            }
+        }
+
         return result;
     }
 
     /**
      * Create an XWikiException object with the given source, export type and error type.
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/pdf/impl/PdfExportImplTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/pdf/impl/PdfExportImplTest.java
index 7532ee3167a93..b273f85446174 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/pdf/impl/PdfExportImplTest.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/pdf/impl/PdfExportImplTest.java
@@ -17,69 +17,120 @@
  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package com.xpn.xwiki.pdf.impl;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.StringReader;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.w3c.dom.Document;
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.environment.Environment;
-import org.xwiki.model.reference.DocumentReferenceResolver;
-import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.job.event.status.JobProgressManager;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.observation.ObservationManager;
+import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.template.TemplateManager;
 import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.user.UserReference;
+import org.xwiki.user.UserReferenceResolver;
+import org.xwiki.velocity.VelocityEngine;
 import org.xwiki.velocity.VelocityManager;
+import org.xwiki.xml.EntityResolver;
+import org.xwiki.xml.html.HTMLCleaner;
+import org.xwiki.xml.html.HTMLCleanerConfiguration;
+import org.xwiki.xml.html.filter.HTMLFilter;
+import org.xwiki.xml.internal.XMLReaderFactoryComponent;
+import org.xwiki.xml.internal.html.DefaultHTMLCleanerConfiguration;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.internal.pdf.XSLFORenderer;
+import com.xpn.xwiki.pdf.api.PdfExport;
 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 com.xpn.xwiki.web.XWikiServletRequestStub;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 /**
  * Unit tests for {@link PdfExportImpl}.
  *
  * @version $Id$
  */
 @ComponentList({
-    org.xwiki.xml.internal.XMLReaderFactoryComponent.class,
+    XMLReaderFactoryComponent.class,
 })
+@ReferenceComponentList
 @OldcoreTest
-public class PdfExportImplTest
+class PdfExportImplTest
 {
+    private static final DocumentReference AUTHOR_REFERENCE = new DocumentReference("xwiki", "XWiki", "XWikiAdmin");
+
+    private static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("xwiki", "XWiki", "PDFClass");
+
     @InjectMockitoOldcore
     private MockitoOldcore oldcore;
 
-    /**
-     * Verify that PDF Export can apply some CSS on the XHTML when that XHTML already has some style defined and in
-     * shorthand notation.
-     */
-    @Test
-    public void applyCSSWhenExistingStyleDefinedUsingShorthandNotation() throws Exception
+    @Mock
+    private VelocityEngine velocityEngine;
+
+    @MockComponent
+    private AuthorizationManager authorizationManager;
+
+    @MockComponent
+    private HTMLCleaner htmlCleaner;
+
+    @MockComponent
+    private AuthorExecutor authorExecutor;
+
+    private String htmlContent;
+
+    private String cssProperties;
+
+    private XWikiContext context;
+
+    private PdfExportImpl pdfExport;
+
+    @BeforeEach
+    void setUp() throws Exception
     {
-        this.oldcore.getMocker().registerMockComponent(DocumentReferenceResolver.TYPE_STRING, "currentmixed");
-        this.oldcore.getMocker().registerMockComponent(EntityReferenceSerializer.TYPE_STRING);
-        this.oldcore.getMocker().registerMockComponent(DocumentAccessBridge.class);
-        this.oldcore.getMocker().registerMockComponent(DocumentAccessBridge.class);
         this.oldcore.getMocker().registerMockComponent(PDFResourceResolver.class);
         this.oldcore.getMocker().registerMockComponent(Environment.class);
-        this.oldcore.getMocker().registerMockComponent(VelocityManager.class);
+        this.oldcore.getMocker().registerMockComponent(TemplateManager.class);
+        this.oldcore.getMocker().registerMockComponent(ObservationManager.class);
+        this.oldcore.getMocker().registerMockComponent(JobProgressManager.class);
+        this.oldcore.getMocker().registerMockComponent(EntityResolver.class);
         this.oldcore.getMocker().registerMockComponent(XSLFORenderer.class, "fop");
-
-        PdfExportImpl pdfExport = new PdfExportImpl();
+        this.oldcore.getMocker().registerMockComponent(HTMLFilter.class, "uniqueId");
 
         // The content below allows us to test several points:
         // 1) The SPAN below already has some style defined in shorthand notation( "background" is shorthand,
         //    see https://www.w3schools.com/css/css_background.asp). That's important for the test since that's what was
         //    failing in the past and why this test was written.
         // 2) We also test that HTML entities are correctly kept since we had issues with this at one point.
-        String html = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+        this.htmlContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
             + "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "
-                + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
+            + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
             + "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n"
             + "<title>\n"
             + "  Main.ttt - ttt\n"
             + "</title>\n"
             + "<meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\" />\n"
@@ -96,43 +147,113 @@ public void applyCSSWhenExistingStyleDefinedUsingShorthandNotation() throws Exce
             + "</div>\n"
             + "</div>\n"
             + "\n"
             + "</body></html>";
 
-        String css = "span { color:red; }";
+        this.cssProperties = "span { color:red; }";
 
-        XWikiContext xcontext = this.oldcore.getXWikiContext();
+        // Set up HTML cleaner.
+        Document htmlDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+            .parse(new ByteArrayInputStream(this.htmlContent.getBytes()));
+        HTMLCleanerConfiguration cleanerConfiguration = new DefaultHTMLCleanerConfiguration();
+        when(this.htmlCleaner.getDefaultConfiguration()).thenReturn(cleanerConfiguration);
+        when(this.htmlCleaner.clean(any(StringReader.class), eq(cleanerConfiguration))).thenReturn(htmlDocument);
+
+        // Get a mocked Velocity Engine.
+        VelocityManager velocityManager = this.oldcore.getMocker().registerMockComponent(VelocityManager.class);
+        when(velocityManager.getVelocityEngine()).thenReturn(this.velocityEngine);
+
+        // Prepare a document reference and author reference for the template.
+        XWikiDocument template = new XWikiDocument(DOCUMENT_REFERENCE);
+        UserReferenceResolver<DocumentReference> userReferenceResolver =
+            this.oldcore.getMocker().getInstance(UserReferenceResolver.TYPE_DOCUMENT_REFERENCE, "document");
+        UserReference userReference = userReferenceResolver.resolve(AUTHOR_REFERENCE);
+        template.getAuthors().setEffectiveMetadataAuthor(userReference);
+
+        // Return a non-empty template property.
+        DocumentAccessBridge dab = this.oldcore.getDocumentAccessBridge();
+        when(dab.getProperty(template.getDocumentReference(), template.getDocumentReference(), "style"))
+            .thenReturn(this.cssProperties);
+        when(dab.getDocumentInstance(DOCUMENT_REFERENCE)).thenReturn(template);
+
+        // Set necessary parameters in the request.
+        this.context = this.oldcore.getXWikiContext();
+        XWikiServletRequestStub request = new XWikiServletRequestStub();
+        request.put("pdftemplate", "XWiki.PDFClass");
+        this.context.setRequest(request);
         XWikiDocument doc = mock(XWikiDocument.class);
-        when(doc.getExternalURL("view", xcontext)).thenReturn("http://localhost:8080/export");
-        xcontext.setDoc(doc);
+        when(doc.getExternalURL("view", this.context)).thenReturn("http://localhost:8080/export");
+        this.context.setDoc(doc);
 
+        this.pdfExport = new PdfExportImpl();
+    }
+
+    /**
+     * Verify that PDF Export can apply some CSS on the XHTML when that XHTML already has some style defined and in
+     * shorthand notation.
+     */
+    @Test
+    void applyCSSWhenExistingStyleDefinedUsingShorthandNotation()
+    {
         // - Verify that element's style attributes are normalized and that the SPAN's color is set to red.
         // - Verify that the accent in the content is still there.
         //   TODO: right now we output the DOM with DOM4J and use the default of converting entities when using the
         //   XMLWriter. We need to decide if that's correct or if we should call XMLWriter#setResolveEntityRefs(false)
         //   instead.
 
         String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
             + "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "
-                + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
-                + "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n"
+            + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
+            + "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n"
             + "<title>\n"
             + "  Main.ttt - ttt\n"
             + "</title>\n"
             + "<meta content=\"text/html; charset=UTF-8\" http-equiv=\"Content-Type\"/>\n"
             + "<meta content=\"en\" name=\"language\"/>\n\n"
             + "</head><body class=\"exportbody\" id=\"body\" pdfcover=\"0\" pdftoc=\"0\">\n\n"
             + "<div id=\"xwikimaincontainer\">\n"
             + "<div id=\"xwikimaincontainerinner\">\n\n"
             + "<div id=\"xwikicontent\">\n"
-                + "      <p><span style=\"color: #f00; background-color: #fff; background-image: none; "
-                + "background-position: 0% 0%; background-size: auto auto; background-origin: padding-box; "
-                + "background-clip: border-box; background-repeat: repeat repeat; "
-                + "background-attachment: scroll; \">Hello Clément</span></p>\n"
+            + "      <p><span style=\"color: #f00; background-color: #fff; background-image: none; "
+            + "background-position: 0% 0%; background-size: auto auto; background-origin: padding-box; "
+            + "background-clip: border-box; background-repeat: repeat repeat; "
+            + "background-attachment: scroll; \">Hello Clément</span></p>\n"
             + "          </div>\n"
             + "</div>\n"
             + "</div>\n\n"
             + "</body></html>";
 
-        assertEquals(expected, pdfExport.applyCSS(html, css, xcontext));
+        assertEquals(expected, this.pdfExport.applyCSS(this.htmlContent, this.cssProperties, this.context));
+    }
+
+    /**
+     * Verify that the Velocity Engine is never accessed if the user does not have script rights.
+     */
+    @Test
+    void applyPDFTemplateWithoutScriptRights() throws Exception
+    {
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE)).thenReturn(false);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        this.pdfExport.exportHtml(this.htmlContent, baos, PdfExport.ExportType.PDF, this.context);
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE);
+        verifyNoInteractions(this.authorExecutor);
+        verifyNoInteractions(this.velocityEngine);
+    }
+
+    /**
+     * Verify that the Velocity Engine is not accessed outside an Author Executor.
+     */
+    @Test
+    void applyPDFTemplateWithAuthorExecutor() throws Exception
+    {
+        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE)).thenReturn(true);
+
+        // Do not call the callable to check that the call to the Velocity engine is inside the author executor.
+        doReturn("").when(this.authorExecutor).call(any(), any(), any());
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        this.pdfExport.exportHtml(this.htmlContent, baos, PdfExport.ExportType.PDF, this.context);
+        verify(this.authorizationManager).hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE);
+        verify(this.authorExecutor).call(any(), eq(AUTHOR_REFERENCE), eq(DOCUMENT_REFERENCE));
+        verifyNoInteractions(this.velocityEngine);
     }
 }

Vulnerable code: import org.apache.commons.lang3.exception.ExceptionUtils;
import org.xwiki.velocity.XWikiVelocityException;
    private static DocumentReferenceResolver<String> referenceResolver =
    private static EntityReferenceSerializer<String> referenceSerializer =
    private static DocumentAccessBridge dab = Utils.getComponent(DocumentAccessBridge.class);
    private static VelocityManager velocityManager = Utils.getComponent(VelocityManager.class);
    private static XMLReaderFactory xmlReaderFactory = Utils.getComponent(XMLReaderFactory.class);
    private Environment environment = Utils.getComponent((Type) Environment.class);
    private XSLFORenderer xslFORenderer = Utils.getComponent(XSLFORenderer.class, "fop");
        List<HTMLFilter> filters = new ArrayList<HTMLFilter>(config.getFilters());
            this.xslFORenderer.render(new ByteArrayInputStream(xmlfo.getBytes("UTF-8")), out, type.getMimeType());
            XMLReader xmlReader = xmlReaderFactory.createXMLReader();
            templateReference = referenceResolver.resolve(pdftemplate);
            templateReference = dab.getCurrentDocumentReference();
            String currentWiki = dab.getCurrentDocumentReference().getRoot().getName();
        String result = (String) dab.getProperty(templateReference, classReference, propertyName);
        if (StringUtils.isBlank(result)) {
        String templateName = referenceSerializer.serialize(templateReference);
            StringWriter writer = new StringWriter();
            VelocityContext vcontext = velocityManager.getVelocityContext();
            velocityManager.getVelocityEngine().evaluate(vcontext, writer, templateName, result);
            result = writer.toString();
        } catch (XWikiVelocityException e) {
            LOGGER.warn("Error applying Velocity to the [{}] property of the [{}] document. Using the property's value "
                + "without applying Velocity.", propertyName, templateName, ExceptionUtils.getRootCauseMessage(e));
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReferenceSerializer;
    org.xwiki.xml.internal.XMLReaderFactoryComponent.class,
public class PdfExportImplTest
    /**
     * Verify that PDF Export can apply some CSS on the XHTML when that XHTML already has some style defined and in
     * shorthand notation.
     */
    @Test
    public void applyCSSWhenExistingStyleDefinedUsingShorthandNotation() throws Exception
        this.oldcore.getMocker().registerMockComponent(DocumentReferenceResolver.TYPE_STRING, "currentmixed");
        this.oldcore.getMocker().registerMockComponent(EntityReferenceSerializer.TYPE_STRING);
        this.oldcore.getMocker().registerMockComponent(DocumentAccessBridge.class);
        this.oldcore.getMocker().registerMockComponent(DocumentAccessBridge.class);
        this.oldcore.getMocker().registerMockComponent(VelocityManager.class);

        PdfExportImpl pdfExport = new PdfExportImpl();
        String html = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
        String css = "span { color:red; }";
        XWikiContext xcontext = this.oldcore.getXWikiContext();
        when(doc.getExternalURL("view", xcontext)).thenReturn("http://localhost:8080/export");
        xcontext.setDoc(doc);
                + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
                + "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n"
                + "      <p><span style=\"color: #f00; background-color: #fff; background-image: none; "
                + "background-position: 0% 0%; background-size: auto auto; background-origin: padding-box; "
                + "background-clip: border-box; background-repeat: repeat repeat; "
                + "background-attachment: scroll; \">Hello Clément</span></p>\n"
        assertEquals(expected, pdfExport.applyCSS(html, css, xcontext));
Fixed code: import java.nio.charset.StandardCharsets;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.user.UserReferenceSerializer;
    private final DocumentReferenceResolver<String> referenceResolver =
    private final EntityReferenceSerializer<String> referenceSerializer =
    private final DocumentAccessBridge dab = Utils.getComponent(DocumentAccessBridge.class);
    private final VelocityManager velocityManager = Utils.getComponent(VelocityManager.class);
    private final XMLReaderFactory xmlReaderFactory = Utils.getComponent(XMLReaderFactory.class);

    private final AuthorizationManager authorizationManager = Utils.getComponent(AuthorizationManager.class);

    private final AuthorExecutor authorExecutor = Utils.getComponent(AuthorExecutor.class);

    private final UserReferenceSerializer<DocumentReference> userReferenceSerializer =
        Utils.getComponent(UserReferenceSerializer.TYPE_DOCUMENT_REFERENCE, "document");
    private final Environment environment = Utils.getComponent((Type) Environment.class);
    private final XSLFORenderer xslFORenderer = Utils.getComponent(XSLFORenderer.class, "fop");
        List<HTMLFilter> filters = new ArrayList<>(config.getFilters());
            this.xslFORenderer.render(new ByteArrayInputStream(xmlfo.getBytes(StandardCharsets.UTF_8)), out, type.getMimeType());
            XMLReader xmlReader = this.xmlReaderFactory.createXMLReader();
            templateReference = this.referenceResolver.resolve(pdftemplate);
            templateReference = this.dab.getCurrentDocumentReference();
            String currentWiki = this.dab.getCurrentDocumentReference().getRoot().getName();
        String templateContent = (String) this.dab.getProperty(templateReference, classReference, propertyName);
        if (StringUtils.isBlank(templateContent)) {

        String templateName = this.referenceSerializer.serialize(templateReference);
        DocumentReference templateAuthorReference;
        String result = templateContent;
             templateAuthorReference = this.userReferenceSerializer.serialize(
                 this.dab.getDocumentInstance(templateReference).getAuthors().getEffectiveMetadataAuthor());
        } catch (Exception e) {
            LOGGER.warn("Error fetching the author of template [{}] during PDF conversion. Using the [{}] property of "
                + "the document's value without applying Velocity.", templateName, propertyName);
            return result;

        if (this.authorizationManager.hasAccess(Right.SCRIPT, templateAuthorReference, templateReference)) {
            try {
                result = this.authorExecutor.call(() -> {
                    StringWriter writer = new StringWriter();
                    VelocityContext vcontext = this.velocityManager.getVelocityContext();
                    this.velocityManager.getVelocityEngine().evaluate(vcontext, writer, templateName,
                        templateContent);
                    return writer.toString();
                }, templateAuthorReference, templateReference);
            } catch (Exception e) {
                LOGGER.warn("Failed to run Velocity engine in author executor. Using the [{}] property of the [{}] "
                    + "document's value without applying Velocity. Reason: [{}]",
                    propertyName, templateName, ExceptionUtils.getRootCauseMessage(e));
            }
        }

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringReader;

import javax.xml.parsers.DocumentBuilderFactory;

import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mock;
import org.w3c.dom.Document;
import org.xwiki.job.event.status.JobProgressManager;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.observation.ObservationManager;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.template.TemplateManager;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceResolver;
import org.xwiki.velocity.VelocityEngine;
import org.xwiki.xml.EntityResolver;
import org.xwiki.xml.html.HTMLCleaner;
import org.xwiki.xml.html.HTMLCleanerConfiguration;
import org.xwiki.xml.html.filter.HTMLFilter;
import org.xwiki.xml.internal.XMLReaderFactoryComponent;
import org.xwiki.xml.internal.html.DefaultHTMLCleanerConfiguration;
import com.xpn.xwiki.pdf.api.PdfExport;
import com.xpn.xwiki.test.reference.ReferenceComponentList;
import com.xpn.xwiki.web.XWikiServletRequestStub;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
    XMLReaderFactoryComponent.class,
@ReferenceComponentList
class PdfExportImplTest
    private static final DocumentReference AUTHOR_REFERENCE = new DocumentReference("xwiki", "XWiki", "XWikiAdmin");

    private static final DocumentReference DOCUMENT_REFERENCE = new DocumentReference("xwiki", "XWiki", "PDFClass");

    @Mock
    private VelocityEngine velocityEngine;

    @MockComponent
    private AuthorizationManager authorizationManager;

    @MockComponent
    private HTMLCleaner htmlCleaner;

    @MockComponent
    private AuthorExecutor authorExecutor;

    private String htmlContent;

    private String cssProperties;

    private XWikiContext context;

    private PdfExportImpl pdfExport;

    @BeforeEach
    void setUp() throws Exception
        this.oldcore.getMocker().registerMockComponent(TemplateManager.class);
        this.oldcore.getMocker().registerMockComponent(ObservationManager.class);
        this.oldcore.getMocker().registerMockComponent(JobProgressManager.class);
        this.oldcore.getMocker().registerMockComponent(EntityResolver.class);
        this.oldcore.getMocker().registerMockComponent(HTMLFilter.class, "uniqueId");
        this.htmlContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
            + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n"
        this.cssProperties = "span { color:red; }";
        // Set up HTML cleaner.
        Document htmlDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()
            .parse(new ByteArrayInputStream(this.htmlContent.getBytes()));
        HTMLCleanerConfiguration cleanerConfiguration = new DefaultHTMLCleanerConfiguration();
        when(this.htmlCleaner.getDefaultConfiguration()).thenReturn(cleanerConfiguration);
        when(this.htmlCleaner.clean(any(StringReader.class), eq(cleanerConfiguration))).thenReturn(htmlDocument);

        // Get a mocked Velocity Engine.
        VelocityManager velocityManager = this.oldcore.getMocker().registerMockComponent(VelocityManager.class);
        when(velocityManager.getVelocityEngine()).thenReturn(this.velocityEngine);

        // Prepare a document reference and author reference for the template.
        XWikiDocument template = new XWikiDocument(DOCUMENT_REFERENCE);
        UserReferenceResolver<DocumentReference> userReferenceResolver =
            this.oldcore.getMocker().getInstance(UserReferenceResolver.TYPE_DOCUMENT_REFERENCE, "document");
        UserReference userReference = userReferenceResolver.resolve(AUTHOR_REFERENCE);
        template.getAuthors().setEffectiveMetadataAuthor(userReference);

        // Return a non-empty template property.
        DocumentAccessBridge dab = this.oldcore.getDocumentAccessBridge();
        when(dab.getProperty(template.getDocumentReference(), template.getDocumentReference(), "style"))
            .thenReturn(this.cssProperties);
        when(dab.getDocumentInstance(DOCUMENT_REFERENCE)).thenReturn(template);

        // Set necessary parameters in the request.
        this.context = this.oldcore.getXWikiContext();
        XWikiServletRequestStub request = new XWikiServletRequestStub();
        request.put("pdftemplate", "XWiki.PDFClass");
        this.context.setRequest(request);
        when(doc.getExternalURL("view", this.context)).thenReturn("http://localhost:8080/export");
        this.context.setDoc(doc);
        this.pdfExport = new PdfExportImpl();
    }

    /**
     * Verify that PDF Export can apply some CSS on the XHTML when that XHTML already has some style defined and in
     * shorthand notation.
     */
    @Test
    void applyCSSWhenExistingStyleDefinedUsingShorthandNotation()
    {
            + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
            + "<html xmlns=\"http://www.w3.org/1999/xhtml\"><head>\n"
            + "      <p><span style=\"color: #f00; background-color: #fff; background-image: none; "
            + "background-position: 0% 0%; background-size: auto auto; background-origin: padding-box; "
            + "background-clip: border-box; background-repeat: repeat repeat; "
            + "background-attachment: scroll; \">Hello Clément</span></p>\n"
        assertEquals(expected, this.pdfExport.applyCSS(this.htmlContent, this.cssProperties, this.context));
    }

    /**
     * Verify that the Velocity Engine is never accessed if the user does not have script rights.
     */
    @Test
    void applyPDFTemplateWithoutScriptRights() throws Exception
    {
        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE)).thenReturn(false);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        this.pdfExport.exportHtml(this.htmlContent, baos, PdfExport.ExportType.PDF, this.context);
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE);
        verifyNoInteractions(this.authorExecutor);
        verifyNoInteractions(this.velocityEngine);
    }

    /**
     * Verify that the Velocity Engine is not accessed outside an Author Executor.
     */
    @Test
    void applyPDFTemplateWithAuthorExecutor() throws Exception
    {
        when(this.authorizationManager.hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE)).thenReturn(true);

        // Do not call the callable to check that the call to the Velocity engine is inside the author executor.
        doReturn("").when(this.authorExecutor).call(any(), any(), any());

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        this.pdfExport.exportHtml(this.htmlContent, baos, PdfExport.ExportType.PDF, this.context);
        verify(this.authorizationManager).hasAccess(Right.SCRIPT, AUTHOR_REFERENCE, DOCUMENT_REFERENCE);
        verify(this.authorExecutor).call(any(), eq(AUTHOR_REFERENCE), eq(DOCUMENT_REFERENCE));
        verifyNoInteractions(this.velocityEngine);