Skip to content

Check Task type before instantiating scheduler task classes#6325

Open
dkayiwa wants to merge 2 commits into
openmrs:masterfrom
dkayiwa:security/scheduler-typecheck-before-instantiation
Open

Check Task type before instantiating scheduler task classes#6325
dkayiwa wants to merge 2 commits into
openmrs:masterfrom
dkayiwa:security/scheduler-typecheck-before-instantiation

Conversation

@dkayiwa

@dkayiwa dkayiwa commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

LegacyTask.execute and SchedulerFormValidator.validate load the configured task_class_name through OpenmrsClassLoader and call newInstance() on it before checking whether the result implements Task:

  • api/.../scheduler/LegacyTask.javataskClass.getDeclaredConstructor().newInstance() then if (instance instanceof Task)
  • api/.../validator/SchedulerFormValidator.javataskClass.newInstance() then if (!(o instanceof Task))

So configuring a scheduler task with any fully-qualified class name on the classpath runs that class's constructor and static initializer as a side effect, even when it isn't a Task.

Change

Check Task.class.isAssignableFrom(taskClass) first, and only instantiate classes that implement Task. The validator still instantiates a genuine Task afterward so the existing instantiation/access error reporting is preserved.

Severity

Reaching either path requires the Manage Scheduler privilege, and such a user can already schedule a real Task that runs arbitrary code, so this is defense in depth (no new privilege boundary crossed) rather than a critical bug.

Tests

  • New validate_shouldRejectANonTaskClassBeforeInstantiatingIt uses a sentinel non-Task class with a constructor side effect and asserts it is rejected without being instantiated.
  • The existing validate_shouldFailValidationIfClassCannotBeInstantiated now points at an abstract Task subtype so the instantiation-failure branch is still exercised after the type check.

Addresses GHSA-6ggw-67pv-j87p.

LegacyTask.execute and SchedulerFormValidator.validate loaded the
configured task class name and called newInstance() on it before checking
whether it implements Task, so pointing a scheduler task at any class on
the classpath ran that class's constructor and static initializer as a
side effect. Check Task.class.isAssignableFrom(taskClass) first and only
instantiate classes that implement Task.

Reaching this requires the Manage Scheduler privilege (such a user can
already run arbitrary Task code), so this is defense in depth rather than
a privilege boundary.

Adds a regression test proving a non-Task class is rejected before it is
instantiated, and repoints the existing instantiation-failure test at an
abstract Task subtype so that branch stays covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.41%. Comparing base (d8fe136) to head (e9e1fde).

Files with missing lines Patch % Lines
...rc/main/java/org/openmrs/scheduler/LegacyTask.java 0.00% 4 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #6325      +/-   ##
============================================
- Coverage     59.47%   59.41%   -0.07%     
+ Complexity     9541     9536       -5     
============================================
  Files           731      731              
  Lines         38323    38321       -2     
  Branches       5587     5587              
============================================
- Hits          22794    22769      -25     
- Misses        13489    13521      +32     
+ Partials       2040     2031       -9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

JobRunrSchedulerService.saveTaskDefinition/scheduleTask do not run
SchedulerFormValidator, so a TaskDefinition naming a class that does not
implement Task can be persisted and reach LegacyTask.execute unvalidated.
LegacyTask's own Task.class.isAssignableFrom check is the runtime guard on
that path and had no test coverage. This adds LegacyTaskTest, which reuses
the existing ConstructorSideEffectNonTask probe to assert the class is
rejected with a TaskException before it is ever instantiated (its
constructor side effect never runs). The probe's flag is widened to public
so the new test in org.openmrs.scheduler can read it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpKNfxm3QjwUBDePEtFm2H
@sonarqubecloud

Copy link
Copy Markdown

@ibacher ibacher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely an improvement, but see my comment below. I don't think we've regarded side-effects of constructors as load-bearing, especially when we assume it's a no-args constructor.

new Object[] { taskDefinition.getTaskClass(), Task.class.getName() },
"Class does not implement Task interface");
} else {
taskClass.newInstance();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um... what's the point of calling newInstance() here when we do exactly nothing with the instance?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants