CVE-2024-31981 (Java, published 2024-04-10T20:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/xwiki/xwiki-platform
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 message: XWIKI-21337: Apply PDF templates with the rights of their authors


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);