VYPR
Moderate severityNVD Advisory· Published Dec 12, 2024· Updated Dec 13, 2024

XWiki's scheduler in subwiki allows scheduling operations for any main wiki user

CVE-2024-55876

Description

XWiki Platform is a generic wiki platform. Starting in version 1.2-milestone-2 and prior to versions 15.10.9 and 16.3.0, any user with an account on the main wiki could run scheduling operations on subwikis. To reproduce, as a user on the main wiki without any special right, view the document Scheduler.WebHome in a subwiki. Then, click on any operation (*e.g.,* Trigger) on any job. If the operation is successful, then the instance is vulnerable. This has been patched in XWiki 15.10.9 and 16.3.0. As a workaround, those who have subwikis where the Job Scheduler is enabled can edit the objects on Scheduler.WebPreferences to match the patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.xwiki.platform:xwiki-platform-scheduler-uiMaven
>= 1.2-milestone-2, < 15.10.915.10.9
org.xwiki.platform:xwiki-platform-scheduler-uiMaven
>= 16.0.0-rc-1, < 16.3.016.3.0

Affected products

1

Patches

1
54bcc5a7a2e4

XWIKI-21663: Improve Scheduler access

https://github.com/xwiki/xwiki-platformPierre JeanjeanApr 26, 2024via ghsa
5 files changed · +82 190
  • xwiki-platform-core/xwiki-platform-oldcore/src/main/resources/ApplicationResources.properties+1 0 modified
    @@ -2896,6 +2896,7 @@ xe.scheduler.job.backtolist=Back to the job list
     xe.scheduler.job.object=This sheet must be applied to a page that holds a scheduler job object.
     xe.scheduler.updateJobClassComment=Created/Updated Scheduler Job Class definition
     xe.scheduler.invalidToken=Invalid token, please try again by clicking on the desired action below.
    +xe.scheduler.missingProgrammingRights=This action requires programming rights.
     
     ### Statistics application
     xe.statistics.activity=Activity Statistics
    
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-ui/pom.xml+21 0 modified
    @@ -97,5 +97,26 @@
           <type>test-jar</type>
           <scope>test</scope>
         </dependency>
    +    <!-- Provides the component list for SecurityAuthorizationScriptService. -->
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-security-authorization-script</artifactId>
    +      <version>${project.version}</version>
    +      <scope>test</scope>
    +    </dependency>
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-security-authorization-script</artifactId>
    +      <version>${project.version}</version>
    +      <type>test-jar</type>
    +      <scope>test</scope>
    +    </dependency>
    +    <!-- Provides TranslationMacro -->
    +    <dependency>
    +      <groupId>org.xwiki.platform</groupId>
    +      <artifactId>xwiki-platform-localization-macro</artifactId>
    +      <version>${project.version}</version>
    +      <scope>test</scope>
    +    </dependency>
       </dependencies>
     </project>
    
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-ui/src/main/resources/Scheduler/WebHome.xml+30 23 modified
    @@ -41,7 +41,8 @@
     ##
     #set ($scheduler = $xwiki.scheduler)
     ##
    -## If the sheet is called with an action ($request.do), let us first process this action
    +## If the sheet is called with an action ($request.do), let us first process this action after checking the user has
    +## programming rights.
     ## Possible values are : "schedule", "pause", "resume", "unschedule", "delete"
     ##
     #if ("$!request.do" != '' &amp;&amp; "$!request.which" != '')
    @@ -51,23 +52,29 @@
       #set ($tJobHolder = $request.which)
       #set ($jobDoc = $xwiki.getDocument($tJobHolder))
       #set ($jobObj = $jobDoc.getObject('XWiki.SchedulerJobClass'))
    -  #if (!$services.csrf.isTokenValid($request.form_token))
    +  #if (!$services.security.authorization.hasAccess('programming', $xcontext.userReference, $doc.documentReference))
    +     ##
    +     ## Check that the user has programming rights
    +     ##
    +     {{error}}{{translation key='xe.scheduler.missingProgrammingRights'/}}{{/error}}
    +
    +  #elseif (!$services.csrf.isTokenValid($request.form_token))
          ##
          ## Check that the CSRF token matches the user before any operation
          ##
    -     {{error}}$services.localization.render('xe.scheduler.invalidToken'){{/error}}
    +     {{error}}{{translation key='xe.scheduler.invalidToken'/}}{{/error}}
     
       #elseif ($request.do == 'schedule')
         ##
         ## Schedule a job
         ##
         #set ($ok = $scheduler.scheduleJob($jobObj))
         #if (!$ok)
    -      {{error}}$xcontext.get('error'){{/error}}
    +      {{error}}$escapetool.xml($xcontext.get('error')){{/error}}
     
         #else
           #set ($jobName = "$jobObj.get('jobName')")
    -      {{info}}$services.localization.render('xe.scheduler.jobscheduled', [$jobName, $scheduler.getNextFireTime($jobObj)]){{/info}}
    +      {{info}}$escapetool.xml($services.localization.render('xe.scheduler.jobscheduled', [$jobName, $scheduler.getNextFireTime($jobObj)])){{/info}}
     
         #end
       #elseif ($request.do == 'pause')
    @@ -76,10 +83,10 @@
         ##
         #set ($ok = $scheduler.pauseJob($jobObj))
         #if (!$ok)
    -      {{error}}$xcontext.get('error'){{/error}}
    +      {{error}}$escapetool.xml($xcontext.get('error')){{/error}}
     
         #else
    -      {{info}}$services.localization.render('xe.scheduler.paused', [$jobObj.get('jobName')]){{/info}}
    +      {{info}}$escapetool.xml($services.localization.render('xe.scheduler.paused', [$jobObj.get('jobName')])){{/info}}
     
         #end
       #elseif ($request.do == 'resume')
    @@ -88,10 +95,10 @@
         ##
         #set ($ok = $scheduler.resumeJob($jobObj))
         #if (!$ok)
    -      {{error}}$xcontext.get('error'){{/error}}
    +      {{error}}$escapetool.xml($xcontext.get('error')){{/error}}
     
         #else
    -      {{info}}$services.localization.render('xe.scheduler.resumed', [$jobObj.get('jobName'), $scheduler.getNextFireTime($jobObj)]){{/info}}
    +      {{info}}$escapetool.xml($services.localization.render('xe.scheduler.resumed', [$jobObj.get('jobName'), $scheduler.getNextFireTime($jobObj)])){{/info}}
     
         #end
       #elseif ($request.do == 'unschedule')
    @@ -100,10 +107,10 @@
         ##
         #set ($ok = $scheduler.unscheduleJob($jobObj))
         #if (!$ok)
    -      {{error}}$xcontext.get('error'){{/error}}
    +      {{error}}$escapetool.xml($xcontext.get('error')){{/error}}
     
         #else
    -      {{info}}$services.localization.render('xe.scheduler.unscheduled', [$jobObj.get('jobName')]){{/info}}
    +      {{info}}$escapetool.xml($services.localization.render('xe.scheduler.unscheduled', [$jobObj.get('jobName')])){{/info}}
     
         #end
       #elseif ($request.do == 'delete')
    @@ -118,7 +125,7 @@
             #set ($deleteRedirect = $xwiki.getURL($jobObj.getName(), 'delete'))
             $response.sendRedirect($deleteRedirect)
           #else
    -        {{error}}$xcontext.get('error'){{/error}}
    +        {{error}}$escapetool.xml($xcontext.get('error')){{/error}}
     
           #end
         #else
    @@ -131,23 +138,23 @@
         ##
         #set ($ok = $scheduler.triggerJob($jobObj))
         #if (!$ok)
    -      {{error}}$xcontext.get('error'){{/error}}
    +      {{error}}$escapetool.xml($xcontext.get('error')){{/error}}
     
         #else
    -      {{info}}$services.localization.render('xe.scheduler.triggered', [$jobObj.get('jobName')]){{/info}}
    +      {{info}}$escapetool.xml($services.localization.render('xe.scheduler.triggered', [$jobObj.get('jobName')])){{/info}}
     
         #end
       #end
     #end
    -$services.localization.render('xe.scheduler.welcome')
    +{{translation key='xe.scheduler.welcome'/}}
     
    -= $services.localization.render('xe.scheduler.jobs.list') =
    += {{translation key='xe.scheduler.jobs.list'/}} =
     
     ##
     ## Retrieve all scheduler jobs
     ## Display their name, status, possible next fire time, and available actions
     ##
    -|=(%scope="col"%)$services.localization.render('xe.scheduler.jobs.name')|=(%scope="col"%)$services.localization.render('xe.scheduler.jobs.status')|=(%scope="col"%)$services.localization.render('xe.scheduler.jobs.next')|=(%scope="col"%)$services.localization.render('xe.scheduler.jobs.actions')
    +|=(%scope="col"%){{translation key='xe.scheduler.jobs.name'/}}|=(%scope="col"%){{translation key='xe.scheduler.jobs.status'/}}|=(%scope="col"%){{translation key='xe.scheduler.jobs.next'/}}|=(%scope="col"%){{translation key='xe.scheduler.jobs.actions'/}}
     #foreach ($docName in $services.query.xwql('from doc.object(XWiki.SchedulerJobClass) as jobs where doc.fullName &lt;&gt; ''XWiki.SchedulerJobTemplate''').execute())
       #set ($jobHolder = $xwiki.getDocument($docName))
       #set ($job = $jobHolder.getObject('XWiki.SchedulerJobClass'))
    @@ -159,7 +166,7 @@ $services.localization.render('xe.scheduler.welcome')
       #if ($status != 'None')
         #set ($firetime = $scheduler.getNextFireTime($job))
       #else
    -    #set ($firetime = $services.localization.render('xe.scheduler.jobs.next.undefined'))
    +    #set ($firetime = "{{translation key='xe.scheduler.jobs.next.undefined'/}}")
       #end
       #set ($actions = ['trigger'])
       #if ($status == 'None')
    @@ -170,7 +177,7 @@ $services.localization.render('xe.scheduler.welcome')
         #set ($ok = $actions.addAll(['resume', 'unschedule']))
       #end
       #set ($ok = $actions.add('delete'))
    -|$job.get('jobName')|$status|$firetime|**$services.localization.render('xe.scheduler.jobs.actions.access')** [[$services.localization.render('xe.scheduler.jobs.actions.view')&gt;&gt;$services.rendering.escape($jobHolder.fullName, 'xwiki/2.1')]]#if($jobHolder.hasAccessLevel('programming')) [[$services.localization.render('xe.scheduler.jobs.actions.edit')&gt;&gt;path:${jobHolder.getURL('edit')}]]#end **$services.localization.render('xe.scheduler.jobs.actions.manage')**#foreach($action in $actions) [[$services.localization.render("xe.scheduler.jobs.actions.$action")&gt;&gt;path:$doc.getURL('view', $escapetool.url({'do': $action, 'which': $jobHolder.fullName, 'form_token': $services.csrf.token}))]]#end
    +|$job.get('jobName')|$status|$firetime|**{{translation key='xe.scheduler.jobs.actions.access'/}}** [[{{translation key='xe.scheduler.jobs.actions.view'/}}&gt;&gt;$services.rendering.escape($jobHolder.fullName, 'xwiki/2.1')]]#if($jobHolder.hasAccessLevel('programming')) [[{{translation key='xe.scheduler.jobs.actions.edit'/}}&gt;&gt;path:${jobHolder.getURL('edit')}]]#end **{{translation key='xe.scheduler.jobs.actions.manage'/}}**#foreach($action in $actions) [[{{translation key='xe.scheduler.jobs.actions.$action'/}}&gt;&gt;path:$doc.getURL('view', $escapetool.url({'do': $action, 'which': $jobHolder.fullName, 'form_token': $services.csrf.token}))]]#end
     
     #end
     #if ($doc.hasAccessLevel('programming'))
    @@ -181,12 +188,12 @@ $services.localization.render('xe.scheduler.welcome')
     ## schedule, pause, etc.
     ##
     
    -= $services.localization.render('xe.scheduler.jobs.create') =
    += {{translation key='xe.scheduler.jobs.create'/}} =
     
     ##
     ## Form to create a new Job
     ##
    -{{info}}$services.localization.render('xe.scheduler.jobs.explaincreate'){{/info}}
    +{{info}}{{translation key='xe.scheduler.jobs.explaincreate'/}}{{/info}}
     
     {{html}}
     &lt;form action="$doc.getURL('create')" id="newdoc" class="form-inline"&gt;&lt;div&gt;
    @@ -195,7 +202,7 @@ $services.localization.render('xe.scheduler.welcome')
       &lt;input type="hidden" name="template" value="XWiki.SchedulerJobTemplate" /&gt;
       &lt;input type="hidden" name="sheet" value="1" /&gt;
       &lt;input type="hidden" name="space" value="Scheduler"/&gt;
    -  &lt;label class="sr-only" for="page"&gt;$services.localization.render('xe.scheduler.jobs.create.nameTip')&lt;/label&gt;
    +  &lt;label class="sr-only" for="page"&gt;$escapetool.xml($services.localization.render('xe.scheduler.jobs.create.nameTip'))&lt;/label&gt;
       &lt;input id="page" name="page" size="30" type="text"
           placeholder="$escapetool.xml($services.localization.render('xe.scheduler.jobs.create.nameTip'))" /&gt;
       &lt;span class="buttonwrapper"&gt;
    @@ -207,7 +214,7 @@ $services.localization.render('xe.scheduler.welcome')
     
     #else
     
    -  {{warning}}$services.localization.render('xe.scheduler.jobs.warning'){{/warning}}
    +  {{warning}}{{translation key='xe.scheduler.jobs.warning'/}}{{/warning}}
     
     #end
     {{/velocity}}</content>
    
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-ui/src/main/resources/Scheduler/WebPreferences.xml+1 167 modified
    @@ -114,176 +114,10 @@
           <groups>XWiki.XWikiAdminGroup</groups>
         </property>
         <property>
    -      <levels>admin,edit</levels>
    +      <levels>view,edit,comment,delete,admin</levels>
         </property>
         <property>
           <users/>
         </property>
       </object>
    -  <object>
    -    <name>Scheduler.WebPreferences</name>
    -    <number>1</number>
    -    <className>XWiki.XWikiGlobalRights</className>
    -    <guid>0b2c7da3-315a-4a6d-b782-5de0b20b6497</guid>
    -    <class>
    -      <name>XWiki.XWikiGlobalRights</name>
    -      <customClass/>
    -      <customMapping/>
    -      <defaultViewSheet/>
    -      <defaultEditSheet/>
    -      <defaultWeb/>
    -      <nameField/>
    -      <validationScript/>
    -      <allow>
    -        <defaultValue>1</defaultValue>
    -        <disabled>0</disabled>
    -        <displayFormType>select</displayFormType>
    -        <displayType>allow</displayType>
    -        <name>allow</name>
    -        <number>4</number>
    -        <prettyName>Allow/Deny</prettyName>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
    -      </allow>
    -      <groups>
    -        <cache>0</cache>
    -        <disabled>0</disabled>
    -        <displayType>input</displayType>
    -        <multiSelect>1</multiSelect>
    -        <name>groups</name>
    -        <number>1</number>
    -        <picker>1</picker>
    -        <prettyName>Groups</prettyName>
    -        <relationalStorage>0</relationalStorage>
    -        <separator> </separator>
    -        <size>5</size>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.GroupsClass</classType>
    -      </groups>
    -      <levels>
    -        <cache>0</cache>
    -        <disabled>0</disabled>
    -        <displayType>select</displayType>
    -        <multiSelect>1</multiSelect>
    -        <name>levels</name>
    -        <number>2</number>
    -        <prettyName>Levels</prettyName>
    -        <relationalStorage>0</relationalStorage>
    -        <separator> </separator>
    -        <size>3</size>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.LevelsClass</classType>
    -      </levels>
    -      <users>
    -        <cache>0</cache>
    -        <disabled>0</disabled>
    -        <displayType>input</displayType>
    -        <multiSelect>1</multiSelect>
    -        <name>users</name>
    -        <number>3</number>
    -        <picker>1</picker>
    -        <prettyName>Users</prettyName>
    -        <relationalStorage>0</relationalStorage>
    -        <separator> </separator>
    -        <size>5</size>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.UsersClass</classType>
    -      </users>
    -    </class>
    -    <property>
    -      <allow>0</allow>
    -    </property>
    -    <property>
    -      <groups>XWiki.XWikiAllGroup</groups>
    -    </property>
    -    <property>
    -      <levels>view</levels>
    -    </property>
    -    <property>
    -      <users/>
    -    </property>
    -  </object>
    -  <object>
    -    <name>Scheduler.WebPreferences</name>
    -    <number>2</number>
    -    <className>XWiki.XWikiGlobalRights</className>
    -    <guid>fef3f075-e0ed-46d1-991f-680326129a9d</guid>
    -    <class>
    -      <name>XWiki.XWikiGlobalRights</name>
    -      <customClass/>
    -      <customMapping/>
    -      <defaultViewSheet/>
    -      <defaultEditSheet/>
    -      <defaultWeb/>
    -      <nameField/>
    -      <validationScript/>
    -      <allow>
    -        <defaultValue>1</defaultValue>
    -        <disabled>0</disabled>
    -        <displayFormType>select</displayFormType>
    -        <displayType>allow</displayType>
    -        <name>allow</name>
    -        <number>4</number>
    -        <prettyName>Allow/Deny</prettyName>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.BooleanClass</classType>
    -      </allow>
    -      <groups>
    -        <cache>0</cache>
    -        <disabled>0</disabled>
    -        <displayType>input</displayType>
    -        <multiSelect>1</multiSelect>
    -        <name>groups</name>
    -        <number>1</number>
    -        <picker>1</picker>
    -        <prettyName>Groups</prettyName>
    -        <relationalStorage>0</relationalStorage>
    -        <separator> </separator>
    -        <size>5</size>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.GroupsClass</classType>
    -      </groups>
    -      <levels>
    -        <cache>0</cache>
    -        <disabled>0</disabled>
    -        <displayType>select</displayType>
    -        <multiSelect>1</multiSelect>
    -        <name>levels</name>
    -        <number>2</number>
    -        <prettyName>Levels</prettyName>
    -        <relationalStorage>0</relationalStorage>
    -        <separator> </separator>
    -        <size>3</size>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.LevelsClass</classType>
    -      </levels>
    -      <users>
    -        <cache>0</cache>
    -        <disabled>0</disabled>
    -        <displayType>input</displayType>
    -        <multiSelect>1</multiSelect>
    -        <name>users</name>
    -        <number>3</number>
    -        <picker>1</picker>
    -        <prettyName>Users</prettyName>
    -        <relationalStorage>0</relationalStorage>
    -        <separator> </separator>
    -        <size>5</size>
    -        <unmodifiable>0</unmodifiable>
    -        <classType>com.xpn.xwiki.objects.classes.UsersClass</classType>
    -      </users>
    -    </class>
    -    <property>
    -      <allow>0</allow>
    -    </property>
    -    <property>
    -      <groups/>
    -    </property>
    -    <property>
    -      <levels>view</levels>
    -    </property>
    -    <property>
    -      <users>XWiki.XWikiGuest</users>
    -    </property>
    -  </object>
     </xwikidoc>
    
  • xwiki-platform-core/xwiki-platform-scheduler/xwiki-platform-scheduler-ui/src/test/java/org/xwiki/scheduler/ui/SchedulerPageTest.java+29 0 modified
    @@ -32,6 +32,7 @@
     import org.mockito.Mock;
     import org.quartz.Trigger;
     import org.xwiki.csrf.script.CSRFTokenScriptService;
    +import org.xwiki.localization.macro.internal.TranslationMacro;
     import org.xwiki.model.reference.DocumentReference;
     import org.xwiki.query.internal.ScriptQuery;
     import org.xwiki.query.script.QueryManagerScriptService;
    @@ -41,6 +42,10 @@
     import org.xwiki.rendering.internal.macro.message.InfoMessageMacro;
     import org.xwiki.rendering.internal.macro.message.WarningMessageMacro;
     import org.xwiki.script.service.ScriptService;
    +import org.xwiki.security.authorization.Right;
    +import org.xwiki.security.authorization.script.SecurityAuthorizationScriptService;
    +import org.xwiki.security.script.SecurityScriptService;
    +import org.xwiki.security.script.SecurityScriptServiceComponentList;
     import org.xwiki.test.annotation.ComponentList;
     import org.xwiki.test.page.HTML50ComponentList;
     import org.xwiki.test.page.PageTest;
    @@ -77,8 +82,10 @@
         ErrorMessageMacro.class,
         SchedulerJobClassDocumentInitializer.class,
         TestNoScriptMacro.class,
    +    TranslationMacro.class,
         WarningMessageMacro.class
     })
    +@SecurityScriptServiceComponentList
     @RenderingScriptServiceComponentList
     @DefaultRenderingConfigurationComponentList
     @HTML50ComponentList
    @@ -98,6 +105,8 @@ class SchedulerPageTest extends PageTest
     
         private CSRFTokenScriptService tokenService;
     
    +    private SecurityAuthorizationScriptService authorizationScriptService;
    +
         private SchedulerPluginApi schedulerPluginApi;
     
         @Mock
    @@ -138,6 +147,9 @@ void setUp() throws Exception
             // Fake programming access level to display the complete page.
             when(this.oldcore.getMockRightService().hasAccessLevel(eq("programming"), anyString(), anyString(),
                 any(XWikiContext.class))).thenReturn(true);
    +        this.authorizationScriptService = this.oldcore.getMocker().getInstance(ScriptService.class,
    +            SecurityScriptService.ROLEHINT + "." + SecurityAuthorizationScriptService.ID);
    +        when(this.authorizationScriptService.hasAccess(eq(Right.PROGRAM), any(), any())).thenReturn(true);
         }
     
         /**
    @@ -178,6 +190,23 @@ void checkValidCSRFToken() throws Exception
             assertTrue(result.getElementsByClass("errormessage").isEmpty());
         }
     
    +    /**
    +     * Check that the trigger operation fails if the user is missing programming rights.
    +     */
    +    @Test
    +    void checkMissingProgrammingRights() throws Exception
    +    {
    +        when(this.authorizationScriptService.hasAccess(eq(Right.PROGRAM), any(), any())).thenReturn(false);
    +        when(this.schedulerPluginApi.triggerJob(this.testJobObjectApi)).thenReturn(true);
    +
    +        this.request.put("do", "trigger");
    +        this.request.put("which", "Scheduler.TestJob");
    +        Document result = renderHTMLPage(SCHEDULER_WEB_HOME);
    +
    +        verify(this.schedulerPluginApi, never()).triggerJob(any(Object.class));
    +        assertEquals("xe.scheduler.missingProgrammingRights", result.getElementsByClass("errormessage").text());
    +    }
    +
         /**
          * List every possible action that can be applied to a job depending on its current status.
          *
    

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.