This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/main by this push:
new d33c89312c2 CAMEL-24554: camel-jolokia-starter - bind to loopback and
reject non-loopback browser origin
d33c89312c2 is described below
commit d33c89312c26d596411b0a4ec889f0daa8675d82
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 13:41:01 2026 +0200
CAMEL-24554: camel-jolokia-starter - bind to loopback and reject
non-loopback browser origin
---
.../camel-jolokia-starter/src/main/doc/intro.adoc | 4 +-
.../camel-jolokia-starter/src/main/doc/usage.adoc | 6 +-
.../JolokiaComponentAutoConfiguration.java | 4 +-
.../springboot/restrictor/CamelRestrictor.java | 66 ++++++++++++++++++
.../JolokiaComponentAutoConfigurationTest.java | 2 +-
.../springboot/JolokiaComponentOriginTest.java | 61 +++++++++++++++++
.../springboot/restrictor/CamelRestrictorTest.java | 79 ++++++++++++++++++++++
.../modules/ROOT/pages/starters/jolokia.adoc | 10 ++-
8 files changed, 226 insertions(+), 6 deletions(-)
diff --git a/components-starter/camel-jolokia-starter/src/main/doc/intro.adoc
b/components-starter/camel-jolokia-starter/src/main/doc/intro.adoc
index 6de559101cb..0d1fa1ed47d 100644
--- a/components-starter/camel-jolokia-starter/src/main/doc/intro.adoc
+++ b/components-starter/camel-jolokia-starter/src/main/doc/intro.adoc
@@ -2,4 +2,6 @@ Spring Boot auto-configuration for Camel Jolokia integration.
The Jolokia Starter integrates https://jolokia.org/[Jolokia] agent
configuration in Spring Boot, wrapping the
https://jolokia.org/reference/html/manual/spring.html[Jolokia Spring Support]
with default configurations to let the application work out-of-the-box without
manually declaring Jolokia servers.
-This starter can be considered an alternative to the
https://jolokia.org/reference/html/manual/agents.html[Jolokia JVM agent]. When
enabled, it exposes the Jolokia endpoint at `http://0.0.0.0:8778/jolokia`.
+This starter can be considered an alternative to the
https://jolokia.org/reference/html/manual/agents.html[Jolokia JVM agent]. When
enabled, it exposes the Jolokia endpoint at `http://127.0.0.1:8778/jolokia`.
The agent ships no
+authenticator, so it binds to loopback by default; exposing it beyond the host
is a conscious step via
+`camel.component.jolokia.server-config.host`, and should be paired with
authentication or a network policy.
diff --git a/components-starter/camel-jolokia-starter/src/main/doc/usage.adoc
b/components-starter/camel-jolokia-starter/src/main/doc/usage.adoc
index e8aa20ee568..151e261acfb 100644
--- a/components-starter/camel-jolokia-starter/src/main/doc/usage.adoc
+++ b/components-starter/camel-jolokia-starter/src/main/doc/usage.adoc
@@ -13,6 +13,10 @@ camel.component.jolokia.server-config.discoveryEnabled=true
To avoid exposing all JMX MBeans (see
https://jolokia.org/reference/html/manual/security.html[Security]
considerations), a default Jolokia
https://jolokia.org/reference/html/manual/security.html#security-restrictor[Restrictor]
is provided that allows only Camel related data and some basic information
from Java.
+The restrictor limits which MBeans are reachable, not what may be done to
them: within the allowed domains, reading and writing attributes and invoking
operations are all permitted, since managing Camel through Jolokia (starting
and stopping routes from Hawtio, for example) is what the starter is for. That
capability is why the agent binds to loopback by default. Browser requests from
origins outside loopback are rejected, so a remote page the user visits cannot
drive the agent.
+
+If the endpoint is exposed beyond the host, put authentication or a network
policy in front of it, or supply a stricter restrictor.
+
You can disable the restrictor with
`camel.component.jolokia.use-camel-restrictor=false` or use your own custom one
with
`camel.component.jolokia.server-config.restrictorClass=org.example.MyRestrictor`.
An example to extend the provided restrictor:
@@ -71,7 +75,7 @@ logging.level.org.jolokia=TRACE
=== Kubernetes Support
-The starter provides default configurations for Kubernetes environments. It
checks for the existence of a certification authority file at
`/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` and, if present,
initializes the server using TLS protocol and client authentication. The
endpoint becomes `https://0.0.0.0:8778/jolokia`.
+The starter provides default configurations for Kubernetes environments. It
checks for the existence of a certification authority file at
`/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` and, if present,
initializes the server using TLS protocol and client authentication. The
endpoint becomes `https://127.0.0.1:8778/jolokia`; set
`camel.component.jolokia.server-config.host` to expose it to the cluster.
You can disable this behaviour with
`camel.component.jolokia.kubernetes-discover=false`.
diff --git
a/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfiguration.java
b/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfiguration.java
index 84ee8eade77..dc46bf78d14 100644
---
a/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfiguration.java
+++
b/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfiguration.java
@@ -84,7 +84,9 @@ public class JolokiaComponentAutoConfiguration {
public SpringJolokiaConfigHolder camelConfigHolder() {
LOG.debug("jolokia configuration from properties");
final SpringJolokiaConfigHolder springJolokiaConfigHolder = new
SpringJolokiaConfigHolder();
- setDefaultConfigValue("host", "0.0.0.0");
+ // bind to loopback like the Jolokia JVM agent does; the agent
ships no authenticator, so exposing it
+ // beyond the host has to be a conscious step via
camel.component.jolokia.server-config.host
+ setDefaultConfigValue("host", "127.0.0.1");
setDefaultConfigValue("autoStart", "true");
if (configuration.isUseCamelRestrictor()
&&
!configuration.getServerConfig().containsKey(ConfigKey.RESTRICTOR_CLASS.getKeyValue()))
{
diff --git
a/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictor.java
b/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictor.java
index 62d2fea82c9..2dfe0c44363 100644
---
a/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictor.java
+++
b/components-starter/camel-jolokia-starter/src/main/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictor.java
@@ -20,11 +20,16 @@ import
org.jolokia.server.core.restrictor.AllowAllRestrictor;
import javax.management.ObjectName;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.List;
import java.util.function.Function;
public class CamelRestrictor extends AllowAllRestrictor {
+ private static final String IPV6_LOOPBACK_COMPRESSED = "::1";
+ private static final String IPV6_LOOPBACK_EXPANDED = "0:0:0:0:0:0:0:1";
+
private final List<String> allowedDomains = List.of("org.apache.camel",
"java.lang", "java.nio", "jboss.threads");
private Function<ObjectName, Boolean> objectNameEvaluator =
@@ -50,6 +55,67 @@ public class CamelRestrictor extends AllowAllRestrictor {
return !objectNameEvaluator.apply(name);
}
+ /**
+ * Rejects browser requests from non-loopback origins.
+ * <p/>
+ * {@link AllowAllRestrictor} permits every origin, which leaves the
agent open to being driven by a remote page
+ * the user happens to visit - the port being on loopback is no
protection against that. Requests from loopback
+ * origins are allowed so local browser clients such as Hawtio continue
to work. A request that carries no Origin
+ * or Referer header is also allowed, so ordinary non-browser clients
such as curl and the Jolokia CLI are
+ * unaffected. This follows the loopback-origin policy used by the
Camel Quarkus Jolokia restrictor.
+ *
+ * @param pOrigin the Origin or Referer header of the request,
or <tt>null</tt> when absent
+ * @param pOnlyWhenStrictCheckingIsEnabled whether Jolokia asks to
apply the check only in strict mode
+ * @return <tt>true</tt> if the request may proceed
+ */
+ @Override
+ public boolean isOriginAllowed(String pOrigin, boolean
pOnlyWhenStrictCheckingIsEnabled) {
+ if (pOrigin == null) {
+ return true;
+ }
+
+ try {
+ return isLoopbackHost(new URI(pOrigin).getHost());
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+
+ private static boolean isLoopbackHost(String host) {
+ if (host == null || host.isEmpty()) {
+ return false;
+ }
+
+ if (host.startsWith("[") && host.endsWith("]")) {
+ host = host.substring(1, host.length() - 1);
+ }
+
+ if ("localhost".equalsIgnoreCase(host) ||
"localhost.localdomain".equalsIgnoreCase(host)) {
+ return true;
+ }
+
+ if (IPV6_LOOPBACK_COMPRESSED.equals(host) ||
IPV6_LOOPBACK_EXPANDED.equals(host)) {
+ return true;
+ }
+
+ String[] octets = host.split("\\.", -1);
+ if (octets.length != 4 || !"127".equals(octets[0])) {
+ return false;
+ }
+
+ try {
+ for (int i = 1; i < octets.length; i++) {
+ int octet = Integer.parseInt(octets[i]);
+ if (octet < 0 || octet > 255) {
+ return false;
+ }
+ }
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
/**
* Provides the list of allowed domains from JMX.
* @return List of String, the list of the allowed domains.
diff --git
a/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfigurationTest.java
b/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfigurationTest.java
index 59d1f2bd19c..36821028d58 100644
---
a/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfigurationTest.java
+++
b/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentAutoConfigurationTest.java
@@ -76,7 +76,7 @@ public class JolokiaComponentAutoConfigurationTest extends
JolokiaComponentTestB
.extracting("configHolder").isNotNull()
.extracting("config")
.asInstanceOf(InstanceOfAssertFactories.map(String.class, String.class))
- .containsEntry("host", "0.0.0.0")
+ .containsEntry("host", "127.0.0.1")
.containsEntry("autoStart", "true")
.containsEntry("restrictorClass",
CamelRestrictor.class.getCanonicalName())
.containsEntry("discoveryEnabled", "true");
diff --git
a/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentOriginTest.java
b/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentOriginTest.java
new file mode 100644
index 00000000000..f17ee5413f9
--- /dev/null
+++
b/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/JolokiaComponentOriginTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.jolokia.springboot;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(classes = { CamelAutoConfiguration.class,
JolokiaComponentAutoConfiguration.class },
+ properties = "camel.component.jolokia.serverConfig.port=0")
+class JolokiaComponentOriginTest extends JolokiaComponentTestBase {
+
+ @Test
+ void allowsRequestFromTheJolokiaServersOwnOrigin() throws Exception {
+ String serverOrigin = "http://127.0.0.1:" +
agent.getAddress().getPort();
+ URI endpoint = URI.create(serverOrigin + "/jolokia");
+ HttpClient client = HttpClient.newHttpClient();
+
+ HttpResponse<String> commandLineResponse = client.send(
+ HttpRequest.newBuilder(endpoint)
+ .header("Content-Type", "application/json")
+
.POST(HttpRequest.BodyPublishers.ofString("{\"type\":\"version\"}"))
+ .build(),
+ HttpResponse.BodyHandlers.ofString());
+ assertThat(commandLineResponse.statusCode()).isEqualTo(200);
+ assertThat(commandLineResponse.body()).contains("\"status\":200");
+
+ HttpResponse<String> browserResponse = client.send(
+ HttpRequest.newBuilder(endpoint)
+ .header("Content-Type", "application/json")
+ .header("Origin", serverOrigin)
+
.POST(HttpRequest.BodyPublishers.ofString("{\"type\":\"version\"}"))
+ .build(),
+ HttpResponse.BodyHandlers.ofString());
+
+ assertThat(browserResponse.body())
+ .as("a same-origin browser request should not be rejected")
+ .contains("\"status\":200");
+ }
+}
diff --git
a/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictorTest.java
b/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictorTest.java
new file mode 100644
index 00000000000..4da3567d870
--- /dev/null
+++
b/components-starter/camel-jolokia-starter/src/test/java/org/apache/camel/component/jolokia/springboot/restrictor/CamelRestrictorTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.jolokia.springboot.restrictor;
+
+import org.junit.jupiter.api.Test;
+
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CamelRestrictorTest {
+
+ private final CamelRestrictor restrictor = new CamelRestrictor();
+
+ private static ObjectName name(String value) throws
MalformedObjectNameException {
+ return new ObjectName(value);
+ }
+
+ @Test
+ void rejectsCrossOriginBrowserRequests() {
+ // AllowAllRestrictor permits every origin, which lets a page
the user visits drive the agent -
+ // binding to loopback is no protection against that
+
assertThat(restrictor.isOriginAllowed("http://evil.example.com",
false)).isFalse();
+
assertThat(restrictor.isOriginAllowed("http://evil.example.com",
true)).isFalse();
+ }
+
+ @Test
+ void allowsLoopbackBrowserOrigins() {
+ assertThat(restrictor.isOriginAllowed("http://localhost:8080",
false)).isTrue();
+ assertThat(restrictor.isOriginAllowed("http://127.0.0.1:8778",
false)).isTrue();
+
assertThat(restrictor.isOriginAllowed("http://127.255.255.255:8778",
false)).isTrue();
+ assertThat(restrictor.isOriginAllowed("http://[::1]:8778",
false)).isTrue();
+
assertThat(restrictor.isOriginAllowed("http://[0:0:0:0:0:0:0:1]:8778",
true)).isTrue();
+ }
+
+ @Test
+ void rejectsMalformedAndNonLoopbackOrigins() {
+ assertThat(restrictor.isOriginAllowed("not a URI",
false)).isFalse();
+
assertThat(restrictor.isOriginAllowed("http://localhost.example.com",
false)).isFalse();
+ assertThat(restrictor.isOriginAllowed("http://128.0.0.1:8778",
false)).isFalse();
+ }
+
+ @Test
+ void allowsRequestsCarryingNoOrigin() {
+ // curl and the Jolokia CLI send no Origin or Referer header
+ assertThat(restrictor.isOriginAllowed(null, false)).isTrue();
+ assertThat(restrictor.isOriginAllowed(null, true)).isTrue();
+ }
+
+ @Test
+ void stillLimitsMBeansToTheAllowedDomains() throws Exception {
+
assertThat(restrictor.isAttributeReadAllowed(name("org.apache.camel:type=context"),
"CamelId")).isTrue();
+
assertThat(restrictor.isAttributeReadAllowed(name("com.example:type=Secret"),
"value")).isFalse();
+
assertThat(restrictor.isObjectNameHidden(name("com.example:type=Secret"))).isTrue();
+ }
+
+ @Test
+ void managementOfCamelMBeansRemainsPossible() throws Exception {
+ // the starter exists to manage Camel through Jolokia, so
operations on the Camel domain stay allowed;
+ // that capability is why the agent binds to loopback by default
+
assertThat(restrictor.isOperationAllowed(name("org.apache.camel:type=context"),
"stop")).isTrue();
+
assertThat(restrictor.isOperationAllowed(name("com.example:type=Secret"),
"reveal")).isFalse();
+ }
+}
diff --git a/docs/spring-boot/modules/ROOT/pages/starters/jolokia.adoc
b/docs/spring-boot/modules/ROOT/pages/starters/jolokia.adoc
index 37594b9854a..128b3eeb207 100644
--- a/docs/spring-boot/modules/ROOT/pages/starters/jolokia.adoc
+++ b/docs/spring-boot/modules/ROOT/pages/starters/jolokia.adoc
@@ -7,7 +7,9 @@ Spring Boot auto-configuration for Camel Jolokia integration.
The Jolokia Starter integrates https://jolokia.org/[Jolokia] agent
configuration in Spring Boot, wrapping the
https://jolokia.org/reference/html/manual/spring.html[Jolokia Spring Support]
with default configurations to let the application work out-of-the-box without
manually declaring Jolokia servers.
-This starter can be considered an alternative to the
https://jolokia.org/reference/html/manual/agents.html[Jolokia JVM agent]. When
enabled, it exposes the Jolokia endpoint at `http://0.0.0.0:8778/jolokia`.
+This starter can be considered an alternative to the
https://jolokia.org/reference/html/manual/agents.html[Jolokia JVM agent]. When
enabled, it exposes the Jolokia endpoint at `http://127.0.0.1:8778/jolokia`.
The agent ships no
+authenticator, so it binds to loopback by default; exposing it beyond the host
is a conscious step via
+`camel.component.jolokia.server-config.host`, and should be paired with
authentication or a network policy.
== Maven coordinates
@@ -36,6 +38,10 @@ camel.component.jolokia.server-config.discoveryEnabled=true
To avoid exposing all JMX MBeans (see
https://jolokia.org/reference/html/manual/security.html[Security]
considerations), a default Jolokia
https://jolokia.org/reference/html/manual/security.html#security-restrictor[Restrictor]
is provided that allows only Camel related data and some basic information
from Java.
+The restrictor limits which MBeans are reachable, not what may be done to
them: within the allowed domains, reading and writing attributes and invoking
operations are all permitted, since managing Camel through Jolokia (starting
and stopping routes from Hawtio, for example) is what the starter is for. That
capability is why the agent binds to loopback by default. Browser requests from
origins outside loopback are rejected, so a remote page the user visits cannot
drive the agent.
+
+If the endpoint is exposed beyond the host, put authentication or a network
policy in front of it, or supply a stricter restrictor.
+
You can disable the restrictor with
`camel.component.jolokia.use-camel-restrictor=false` or use your own custom one
with
`camel.component.jolokia.server-config.restrictorClass=org.example.MyRestrictor`.
An example to extend the provided restrictor:
@@ -94,7 +100,7 @@ logging.level.org.jolokia=TRACE
=== Kubernetes Support
-The starter provides default configurations for Kubernetes environments. It
checks for the existence of a certification authority file at
`/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` and, if present,
initializes the server using TLS protocol and client authentication. The
endpoint becomes `https://0.0.0.0:8778/jolokia`.
+The starter provides default configurations for Kubernetes environments. It
checks for the existence of a certification authority file at
`/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` and, if present,
initializes the server using TLS protocol and client authentication. The
endpoint becomes `https://127.0.0.1:8778/jolokia`; set
`camel.component.jolokia.server-config.host` to expose it to the cluster.
You can disable this behaviour with
`camel.component.jolokia.kubernetes-discover=false`.