VYPR
Critical severityNVD Advisory· Published Aug 24, 2023· Updated Oct 3, 2024

XWiki Platform's Groovy jobs check the wrong author, allowing remote code execution

CVE-2023-40573

Description

XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. XWiki supports scheduled jobs that contain Groovy scripts. Currently, the job checks the content author of the job for programming right. However, modifying or adding a job script to a document doesn't modify the content author. Together with a CSRF vulnerability in the job scheduler, this can be exploited for remote code execution by an attacker with edit right on the wiki. If the attack is successful, an error log entry with "Job content executed" will be produced. This vulnerability has been patched in XWiki 14.10.9 and 15.4RC1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.xwiki.platform:xwiki-platform-scheduler-apiMaven
< 14.10.914.10.9
com.xpn.xwiki.platform.plugins:xwiki-plugin-schedulerMaven
>= 1.3
org.xwiki.platform:xwiki-platform-scheduler-apiMaven
>= 15.0-rc-1, < 15.4-rc-115.4-rc-1

Affected products

1

Patches

1
fcdcfed3fe2e

XWIKI-20852: Improve author checking in GroovyJob

https://github.com/xwiki/xwiki-platformMichael HamannApr 21, 2023via ghsa
3 files changed · +212 19
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/pom.xml+14 1 modified
    @@ -34,7 +34,7 @@
       <properties>
         <!-- Name to display by the Extension Manager -->
         <xwiki.extension.name>Scheduler API</xwiki.extension.name>
    -    <xwiki.jacoco.instructionRatio>0.00</xwiki.jacoco.instructionRatio>
    +    <xwiki.jacoco.instructionRatio>0.08</xwiki.jacoco.instructionRatio>
       </properties>
       <dependencies>
         <dependency>
    @@ -50,6 +50,19 @@
           <groupId>javax.servlet</groupId>
           <artifactId>javax.servlet-api</artifactId>
         </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-oldcore</artifactId>
    +      <version>${project.version}</version>
    +      <type>test-jar</type>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.commons</groupId>
    +      <artifactId>xwiki-commons-tool-test-component</artifactId>
    +      <version>${commons.version}</version>
    +      <scope>test</scope>
    +    </dependency>
       </dependencies>
       <build>
         <plugins>
    
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/main/java/com/xpn/xwiki/plugin/scheduler/GroovyJob.java+41 18 modified
    @@ -19,13 +19,20 @@
      */
     package com.xpn.xwiki.plugin.scheduler;
     
    +import java.util.Objects;
    +
     import org.codehaus.groovy.control.CompilationFailedException;
     import org.quartz.JobDataMap;
     import org.quartz.JobExecutionContext;
     import org.quartz.JobExecutionException;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +import org.xwiki.security.authorization.ContextualAuthorizationManager;
    +import org.xwiki.security.authorization.Right;
     
    +import com.xpn.xwiki.XWikiException;
     import com.xpn.xwiki.doc.XWikiDocument;
     import com.xpn.xwiki.objects.BaseObject;
    +import com.xpn.xwiki.web.Utils;
     
     import groovy.lang.Binding;
     import groovy.lang.GroovyShell;
    @@ -64,32 +71,48 @@ protected void executeJob(JobExecutionContext jobContext) throws JobExecutionExc
                 BaseObject object = (BaseObject) data.get("xjob");
     
                 // Force context document
    -            XWikiDocument jobDocument = getXWikiContext().getWiki().getDocument(object.getName(), getXWikiContext());
    +            XWikiDocument jobDocument = object.getOwnerDocument();
    +            if (jobDocument == null) {
    +                jobDocument = getXWikiContext().getWiki().getDocument(object.getDocumentReference(), getXWikiContext());
    +            }
    +
                 getXWikiContext().setDoc(jobDocument);
    -            getXWikiContext().put("sdoc", jobDocument);
    +            XWikiDocument secureDocument;
    +            // Make sure that the secure document has the correct content author.
    +            if (!Objects.equals(jobDocument.getAuthors().getContentAuthor(),
    +                jobDocument.getAuthors().getEffectiveMetadataAuthor()))
    +            {
    +                secureDocument = jobDocument.clone();
    +                secureDocument.getAuthors().setContentAuthor(jobDocument.getAuthors().getEffectiveMetadataAuthor());
    +            } else {
    +                secureDocument = jobDocument;
    +            }
    +            getXWikiContext().put("sdoc", secureDocument);
     
    -            if (getXWikiContext().getWiki().getRightService().hasProgrammingRights(getXWikiContext())) {
    +            ContextualAuthorizationManager contextualAuthorizationManager =
    +                Utils.getComponent(ContextualAuthorizationManager.class);
    +            contextualAuthorizationManager.checkAccess(Right.PROGRAM);
     
    -                // Make the Job execution data available to the Groovy script
    -                Binding binding = new Binding(data.getWrappedMap());
    +            // Make the Job execution data available to the Groovy script
    +            Binding binding = new Binding(data.getWrappedMap());
     
    -                // Set the right instance of XWikiContext
    -                binding.setProperty("context", getXWikiContext());
    -                binding.setProperty("xcontext", getXWikiContext());
    -                data.put("xwiki", new com.xpn.xwiki.api.XWiki(getXWikiContext().getWiki(), getXWikiContext()));
    +            // Set the right instance of XWikiContext
    +            binding.setProperty("context", getXWikiContext());
    +            binding.setProperty("xcontext", getXWikiContext());
    +            data.put("xwiki", new com.xpn.xwiki.api.XWiki(getXWikiContext().getWiki(), getXWikiContext()));
     
    -                // Execute the Groovy script
    -                GroovyShell shell = new GroovyShell(Thread.currentThread().getContextClassLoader(), binding);
    -                shell.evaluate(object.getLargeStringValue("script"));
    -            } else {
    -                throw new JobExecutionException("The user [" + getXWikiContext().getUser() + "] didn't have "
    -                    + "programming rights when the job [" + jobContext.getJobDetail().getKey() + "] was scheduled.");
    -            }
    +            // Execute the Groovy script
    +            GroovyShell shell = new GroovyShell(Thread.currentThread().getContextClassLoader(), binding);
    +            shell.evaluate(object.getLargeStringValue("script"));
             } catch (CompilationFailedException e) {
                 throw new JobExecutionException(
                     "Failed to execute script for job [" + jobContext.getJobDetail().getKey() + "]", e, true);
    -        } catch (Exception e) {
    -            e.printStackTrace();
    +        } catch (XWikiException e) {
    +            throw new JobExecutionException("Failed to load the document containing the job ["
    +                + jobContext.getJobDetail().getKey() + "]", e, true);
    +        } catch (AccessDeniedException e) {
    +            throw new JobExecutionException("Executing the job [" + jobContext.getJobDetail().getKey() + "] failed "
    +                + "due to insufficient rights.", e, true);
             }
         }
     }
    
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-api/src/test/java/com/xpn/xwiki/plugin/scheduler/GroovyJobTest.java+157 0 added
    @@ -0,0 +1,157 @@
    +/*
    + * 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 com.xpn.xwiki.plugin.scheduler;
    +
    +import java.util.function.Consumer;
    +
    +import javax.inject.Inject;
    +
    +import org.junit.jupiter.params.ParameterizedTest;
    +import org.junit.jupiter.params.provider.ValueSource;
    +import org.mockito.Mock;
    +import org.quartz.JobDataMap;
    +import org.quartz.JobDetail;
    +import org.quartz.JobExecutionContext;
    +import org.quartz.JobExecutionException;
    +import org.quartz.JobKey;
    +import org.xwiki.context.Execution;
    +import org.xwiki.context.internal.DefaultExecution;
    +import org.xwiki.context.internal.DefaultExecutionContextManager;
    +import org.xwiki.model.reference.DocumentReference;
    +import org.xwiki.security.authorization.AccessDeniedException;
    +import org.xwiki.security.authorization.Right;
    +import org.xwiki.test.annotation.ComponentList;
    +import org.xwiki.user.GuestUserReference;
    +import org.xwiki.user.UserReference;
    +
    +import com.xpn.xwiki.XWikiContext;
    +import com.xpn.xwiki.XWikiException;
    +import com.xpn.xwiki.doc.XWikiDocument;
    +import com.xpn.xwiki.internal.ReadOnlyXWikiContextProvider;
    +import com.xpn.xwiki.objects.BaseObject;
    +import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer;
    +import com.xpn.xwiki.test.MockitoOldcore;
    +import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
    +import com.xpn.xwiki.test.reference.ReferenceComponentList;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +import static org.junit.jupiter.api.Assertions.assertNotEquals;
    +import static org.junit.jupiter.api.Assertions.assertThrows;
    +import static org.mockito.ArgumentMatchers.any;
    +import static org.mockito.Mockito.doAnswer;
    +import static org.mockito.Mockito.mock;
    +import static org.mockito.Mockito.never;
    +import static org.mockito.Mockito.verify;
    +import static org.mockito.Mockito.when;
    +
    +/**
    + * Unit test for {@link GroovyJob}.
    + *
    + * @version $Id$
    + */
    +@OldcoreTest
    +@ComponentList({
    +    SchedulerJobClassDocumentInitializer.class,
    +    DefaultExecutionContextManager.class,
    +    DefaultExecution.class,
    +    ReadOnlyXWikiContextProvider.class
    +})
    +@ReferenceComponentList
    +class GroovyJobTest
    +{
    +    private static final String WIKI_NAME = "xwiki";
    +
    +    @Mock
    +    private Consumer<UserReference> feedbackFunction;
    +
    +    @Inject
    +    private Execution execution;
    +
    +    @ParameterizedTest
    +    @ValueSource(booleans = { true, false })
    +    void programmingRightCheck(boolean allowExecution, MockitoOldcore mockitoOldcore) throws XWikiException,
    +        JobExecutionException, AccessDeniedException
    +    {
    +        GroovyJob job = new GroovyJob();
    +
    +        // Mock the job context.
    +        JobExecutionContext context = mock(JobExecutionContext.class);
    +        JobDetail jobDetail = mock(JobDetail.class);
    +        when(context.getJobDetail()).thenReturn(jobDetail);
    +        when(jobDetail.getKey()).thenReturn(new JobKey("Test Job"));
    +        JobDataMap jobDataMap = new JobDataMap();
    +        when(jobDetail.getJobDataMap()).thenReturn(jobDataMap);
    +
    +        // Create a new context to ensure that the context from the job is used.
    +        XWikiContext testContext = new XWikiContext();
    +        jobDataMap.put("context", testContext);
    +        testContext.setWiki(mockitoOldcore.getSpyXWiki());
    +        testContext.setUserReference(new DocumentReference(WIKI_NAME, "XWiki", "ContextUser"));
    +
    +        // Create a mock function that we set on the context and that will then be executed by the job.
    +        testContext.put("feedbackFunction", this.feedbackFunction);
    +
    +        // Create the job document.
    +        XWikiDocument jobDocument = new XWikiDocument(new DocumentReference(WIKI_NAME, "Scheduler", "Job"));
    +        // Set different authors to verify that the correct author is used.
    +        UserReference contentAuthor = mock(UserReference.class);
    +        UserReference jobAuthor = mock(UserReference.class);
    +        jobDocument.getAuthors().setContentAuthor(contentAuthor);
    +        jobDocument.getAuthors().setOriginalMetadataAuthor(GuestUserReference.INSTANCE);
    +        jobDocument.getAuthors().setEffectiveMetadataAuthor(jobAuthor);
    +        BaseObject xObject = jobDocument.newXObject(SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE,
    +            mockitoOldcore.getXWikiContext());
    +        xObject.setLargeStringValue("script",
    +            "xcontext.get('feedbackFunction')"
    +                + ".accept(xcontext.get(com.xpn.xwiki.doc.XWikiDocument.CKEY_SDOC).getAuthors().getContentAuthor())");
    +        mockitoOldcore.getSpyXWiki().saveDocument(jobDocument, mockitoOldcore.getXWikiContext());
    +        jobDataMap.put("xjob", xObject);
    +
    +        doAnswer(invocationOnMock -> {
    +            XWikiContext xWikiContext =
    +                (XWikiContext) this.execution.getContext().getProperty(XWikiContext.EXECUTIONCONTEXT_KEY);
    +            assertEquals(jobDocument, xWikiContext.getDoc());
    +            XWikiDocument sDocument = (XWikiDocument) xWikiContext.get(XWikiDocument.CKEY_SDOC);
    +            assertEquals(jobAuthor, sDocument.getAuthors().getContentAuthor());
    +            assertNotEquals(contentAuthor, sDocument.getAuthors().getContentAuthor());
    +
    +            if (!allowExecution) {
    +                throw new AccessDeniedException(Right.PROGRAM, new DocumentReference(WIKI_NAME, "XWiki", "JobAuthor"),
    +                    sDocument.getDocumentReference());
    +            }
    +            return null;
    +        }).when(mockitoOldcore.getMockContextualAuthorizationManager()).checkAccess(Right.PROGRAM);
    +
    +        if (allowExecution) {
    +            job.execute(context);
    +            verify(this.feedbackFunction).accept(jobAuthor);
    +        } else {
    +            JobExecutionException jobExecutionException =
    +                assertThrows(JobExecutionException.class, () -> job.execute(context));
    +            verify(this.feedbackFunction, never()).accept(any());
    +            assertEquals("Executing the job [DEFAULT.Test Job] failed due to insufficient rights.",
    +                jobExecutionException.getMessage());
    +            assertEquals("Access denied when checking [programming] access to [xwiki:Scheduler.Job] "
    +                + "for user [xwiki:XWiki.JobAuthor]", jobExecutionException.getCause().getMessage());
    +        }
    +
    +        verify(mockitoOldcore.getMockContextualAuthorizationManager()).checkAccess(Right.PROGRAM);
    +    }
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.