version: 3.0.0.6
Summary
CMAK gates the ability to schedule (and cancel) a recurring preferred-replica-leader-election job behind the KMScheduleLeaderElectionFeature toggle in application.features, so a deployer can disable this specific capability while leaving other admin functions (including one-off manual leader election) enabled. The JSON API routes for this feature correctly enforce the toggle, but the HTML form routes that implement the exact same operation do not check it at all. When an operator disables KMScheduleLeaderElectionFeature, any caller who can reach CMAK's web endpoints can still start and stop the recurring election scheduler through the plain HTML form POST routes, completely bypassing the operator's intended restriction.
Details
app/controllers/PreferredReplicaElection.scala implements this operation twice: once for the browser-facing HTML form, once for the JSON API. Both pairs of actions call the same underlying kafkaManager.schedulePreferredLeaderElection / kafkaManager.cancelPreferredLeaderElection methods, but only the API pair is wrapped in featureGate(KMScheduleLeaderElectionFeature):
// HTML form route — conf/routes: POST /clusters/:c/leader/schedule
def handleScheduleRunElection(c: String) = Action.async { implicit request =>
def setOrExtract : (Int, String) = if(!kafkaManager.pleCancellable.contains(c)){
kafkaManager.getTopicList(c).flatMap { errorOrTopicList =>
errorOrTopicList.fold({ e =>
Future.successful(-\/(e))
}, { topicList =>
kafkaManager.schedulePreferredLeaderElection(c, topicList.list.toSet, request.body.asFormUrlEncoded.get("timePeriod")(0).toInt)
})
}
(request.body.asFormUrlEncoded.get("timePeriod")(0).toInt, "Scheduler started")
}
else{
(kafkaManager.pleCancellable(c)._2, "Scheduler already scheduled")
}
...
}
// HTML form route — conf/routes: POST /clusters/:c/leader/schedule/cancel
def cancelScheduleRunElection(c: String) = Action.async { implicit request =>
val status_string: String = if(kafkaManager.pleCancellable.contains(c)){
kafkaManager.cancelPreferredLeaderElection(c)
"Scheduler stopped"
}
else "Scheduler already not running"
...
}
Neither method contains a featureGate call anywhere in its body. Contrast with their API siblings a few lines below in the same file:
// JSON API route — conf/routes: POST /api/clusters/:c/leader/schedule
def handleScheduleRunElectionAPI(c: String) = Action.async { implicit request =>
// ToDo: Refactor out common part from handleScheduleRunElection
featureGate(KMScheduleLeaderElectionFeature) {
def setOrExtract : (Int, String) = if(!kafkaManager.pleCancellable.contains(c)){
... kafkaManager.schedulePreferredLeaderElection(...) ...
}
...
}
}
// JSON API route — conf/routes: POST /api/clusters/:c/leader/schedule/cancel
def cancelScheduleRunElectionAPI(c: String) = Action.async { implicit request =>
// ToDo: Refactor out common part from cancelScheduleRunElection
featureGate(KMScheduleLeaderElectionFeature) {
... kafkaManager.cancelPreferredLeaderElection(c) ...
}
}
The // ToDo: Refactor out common part comments on both API methods confirm the two code paths are known duplicates of each other, which is how the gate was added to one copy and missed on the other. featureGate (app/controllers/package.scala) is CMAK's only mechanism for restricting an admin capability app-wide; when the configured feature is not in application.features, it returns an "alert-danger — Feature disabled" page/response and never calls the wrapped block. Since the HTML routes never call featureGate at all, they are unaffected by the toggle regardless of its value.
This bug has been present unmodified since it was introduced (commit fd5517e70125da48982d5a4eed7799eabbc56cc4, April 2019) through the latest tagged release (3.0.0.6) and the newest commit on the default branch (30abfde3, December 2022) — it is not a regression fixed anywhere in the project's history.
PoC
(available upon request)
Impact
An operator who disables KMScheduleLeaderElectionFeature intends to prevent anyone reaching CMAK from starting a recurring, unattended preferred-replica-election job against a managed Kafka cluster (the one-off manual election, gated separately by KMPreferredReplicaElectionFeature, can remain enabled independently). Because the HTML form routes ignore the toggle entirely, that restriction is silently ineffective: the same caller who could reach any other write endpoint in the panel can still turn the recurring scheduler on and off through POST /clusters/:c/leader/schedule and POST /clusters/:c/leader/schedule/cancel. A running scheduler repeatedly triggers cluster-wide preferred-leader elections at a configurable interval with no further confirmation step, causing recurring partition leadership churn and replication/ISR disruption for as long as it runs (bounded by an operator noticing and restarting CMAK, since the API-based cancel path is also blocked by the same disabled feature and the HTML cancel route is the only way to stop it once started this way). This is a missing-authorization defect in the access-control mechanism itself (CWE-862): the security control exists and is exercised correctly by one code path and silently skipped by its functional twin.
version: 3.0.0.6
Summary
CMAK gates the ability to schedule (and cancel) a recurring preferred-replica-leader-election job behind the
KMScheduleLeaderElectionFeaturetoggle inapplication.features, so a deployer can disable this specific capability while leaving other admin functions (including one-off manual leader election) enabled. The JSON API routes for this feature correctly enforce the toggle, but the HTML form routes that implement the exact same operation do not check it at all. When an operator disablesKMScheduleLeaderElectionFeature, any caller who can reach CMAK's web endpoints can still start and stop the recurring election scheduler through the plain HTML form POST routes, completely bypassing the operator's intended restriction.Details
app/controllers/PreferredReplicaElection.scalaimplements this operation twice: once for the browser-facing HTML form, once for the JSON API. Both pairs of actions call the same underlyingkafkaManager.schedulePreferredLeaderElection/kafkaManager.cancelPreferredLeaderElectionmethods, but only the API pair is wrapped infeatureGate(KMScheduleLeaderElectionFeature):Neither method contains a
featureGatecall anywhere in its body. Contrast with their API siblings a few lines below in the same file:The
// ToDo: Refactor out common partcomments on both API methods confirm the two code paths are known duplicates of each other, which is how the gate was added to one copy and missed on the other.featureGate(app/controllers/package.scala) is CMAK's only mechanism for restricting an admin capability app-wide; when the configured feature is not inapplication.features, it returns an "alert-danger — Feature disabled" page/response and never calls the wrapped block. Since the HTML routes never callfeatureGateat all, they are unaffected by the toggle regardless of its value.This bug has been present unmodified since it was introduced (commit
fd5517e70125da48982d5a4eed7799eabbc56cc4, April 2019) through the latest tagged release (3.0.0.6) and the newest commit on the default branch (30abfde3, December 2022) — it is not a regression fixed anywhere in the project's history.PoC
(available upon request)
Impact
An operator who disables
KMScheduleLeaderElectionFeatureintends to prevent anyone reaching CMAK from starting a recurring, unattended preferred-replica-election job against a managed Kafka cluster (the one-off manual election, gated separately byKMPreferredReplicaElectionFeature, can remain enabled independently). Because the HTML form routes ignore the toggle entirely, that restriction is silently ineffective: the same caller who could reach any other write endpoint in the panel can still turn the recurring scheduler on and off throughPOST /clusters/:c/leader/scheduleandPOST /clusters/:c/leader/schedule/cancel. A running scheduler repeatedly triggers cluster-wide preferred-leader elections at a configurable interval with no further confirmation step, causing recurring partition leadership churn and replication/ISR disruption for as long as it runs (bounded by an operator noticing and restarting CMAK, since the API-based cancel path is also blocked by the same disabled feature and the HTML cancel route is the only way to stop it once started this way). This is a missing-authorization defect in the access-control mechanism itself (CWE-862): the security control exists and is exercised correctly by one code path and silently skipped by its functional twin.