This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 2014bec6fee1 CAMEL-24297: make ldif URL-body dereferencing an explicit
opt-in (#25310)
2014bec6fee1 is described below
commit 2014bec6fee1946eb3ad9018523ecdc3644f70ae
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Aug 3 18:57:28 2026 +0200
CAMEL-24297: make ldif URL-body dereferencing an explicit opt-in (#25310)
The ldif producer treated a message body that does not start with "version:
1" as a URL and
dereferenced it (URI.create(body).toURL().openStream()). This
content-sniffed URL fetch is now gated
by a new allowUrlBody option (default false, tagged
security="insecure:dev"). With the default a
non-LDIF body is rejected with an IllegalArgumentException instead of being
fetched, avoiding a
content-sniffed URL fetch (SSRF) from untrusted body content.
Routes that rely on passing a URL as the body must set allowUrlBody=true.
Adds a unit test for the
default rejection, enables the option on the existing LdifRouteIT (which
feeds URLs as the body), and
documents the change in the upgrade guide.
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
.../org/apache/camel/catalog/components/ldif.json | 3 +-
.../component/ldif/LdifEndpointConfigurer.java | 6 +++
.../component/ldif/LdifEndpointUriFactory.java | 3 +-
.../org/apache/camel/component/ldif/ldif.json | 3 +-
.../apache/camel/component/ldif/LdifEndpoint.java | 17 +++++++
.../apache/camel/component/ldif/LdifProducer.java | 11 ++++-
.../camel/component/ldif/LdifAllowUrlBodyTest.java | 53 ++++++++++++++++++++++
.../apache/camel/component/ldif/LdifRouteIT.java | 3 +-
.../java/org/apache/camel/util/SecurityUtils.java | 1 +
.../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 9 ++++
.../endpoint/dsl/LdifEndpointBuilderFactory.java | 40 ++++++++++++++++
11 files changed, 143 insertions(+), 6 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
index 73c2de0d9330..009d5314aefe 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ldif.json
@@ -29,6 +29,7 @@
},
"properties": {
"ldapConnectionName": { "index": 0, "kind": "path", "displayName": "Ldap
Connection Name", "group": "producer", "label": "", "required": true, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
name of the LdapConnection bean to pull from the registry. Note that this must
be of scope prototype to avoid it being shared among threads or using a
connection that has timed out." },
- "lazyStartProducer": { "index": 1, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "lazyStartProducer": { "index": 1, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "allowUrlBody": { "index": 2, "kind": "parameter", "displayName": "Allow
Url Body", "group": "security", "label": "security", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "security": "insecure:dev", "defaultValue": false,
"description": "Whether to allow a message body that is not LDIF content to be
dereferenced as a URL and fetched. When disabled (default), a body that does
not start with version: 1 is rejecte [...]
}
}
diff --git
a/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointConfigurer.java
b/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointConfigurer.java
index b7a30cf4d9ca..d4cecef71358 100644
---
a/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointConfigurer.java
+++
b/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointConfigurer.java
@@ -23,6 +23,8 @@ public class LdifEndpointConfigurer extends
PropertyConfigurerSupport implements
public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
LdifEndpoint target = (LdifEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowurlbody":
+ case "allowUrlBody": target.setAllowUrlBody(property(camelContext,
boolean.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer":
target.setLazyStartProducer(property(camelContext, boolean.class, value));
return true;
default: return false;
@@ -32,6 +34,8 @@ public class LdifEndpointConfigurer extends
PropertyConfigurerSupport implements
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowurlbody":
+ case "allowUrlBody": return boolean.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
default: return null;
@@ -42,6 +46,8 @@ public class LdifEndpointConfigurer extends
PropertyConfigurerSupport implements
public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
LdifEndpoint target = (LdifEndpoint) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowurlbody":
+ case "allowUrlBody": return target.isAllowUrlBody();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
default: return null;
diff --git
a/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointUriFactory.java
b/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointUriFactory.java
index 679fba82585b..4fad9d700596 100644
---
a/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointUriFactory.java
+++
b/components/camel-ldif/src/generated/java/org/apache/camel/component/ldif/LdifEndpointUriFactory.java
@@ -24,7 +24,8 @@ public class LdifEndpointUriFactory extends
org.apache.camel.support.component.E
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
- Set<String> props = new HashSet<>(2);
+ Set<String> props = new HashSet<>(3);
+ props.add("allowUrlBody");
props.add("lazyStartProducer");
props.add("ldapConnectionName");
PROPERTY_NAMES = Collections.unmodifiableSet(props);
diff --git
a/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
b/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
index 73c2de0d9330..009d5314aefe 100644
---
a/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
+++
b/components/camel-ldif/src/generated/resources/META-INF/org/apache/camel/component/ldif/ldif.json
@@ -29,6 +29,7 @@
},
"properties": {
"ldapConnectionName": { "index": 0, "kind": "path", "displayName": "Ldap
Connection Name", "group": "producer", "label": "", "required": true, "type":
"string", "javaType": "java.lang.String", "deprecated": false,
"deprecationNote": "", "autowired": false, "secret": false, "description": "The
name of the LdapConnection bean to pull from the registry. Note that this must
be of scope prototype to avoid it being shared among threads or using a
connection that has timed out." },
- "lazyStartProducer": { "index": 1, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "lazyStartProducer": { "index": 1, "kind": "parameter", "displayName":
"Lazy Start Producer", "group": "producer (advanced)", "label":
"producer,advanced", "required": false, "type": "boolean", "javaType":
"boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": false, "description": "Whether the producer should be started
lazy (on the first message). By starting lazy you can use this to allow
CamelContext and routes to startup in situations where a produc [...]
+ "allowUrlBody": { "index": 2, "kind": "parameter", "displayName": "Allow
Url Body", "group": "security", "label": "security", "required": false, "type":
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false,
"secret": false, "security": "insecure:dev", "defaultValue": false,
"description": "Whether to allow a message body that is not LDIF content to be
dereferenced as a URL and fetched. When disabled (default), a body that does
not start with version: 1 is rejecte [...]
}
}
diff --git
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
index 27f3f12fc194..fda73cbb5fa0 100644
---
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
+++
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifEndpoint.java
@@ -22,6 +22,7 @@ import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.apache.camel.support.DefaultEndpoint;
@@ -34,6 +35,8 @@ public class LdifEndpoint extends DefaultEndpoint {
@UriPath
@Metadata(required = true)
private String ldapConnectionName;
+ @UriParam(label = "security", defaultValue = "false", security =
"insecure:dev")
+ private boolean allowUrlBody;
protected LdifEndpoint(String endpointUri, String remaining, LdifComponent
component) {
super(endpointUri, component);
@@ -61,4 +64,18 @@ public class LdifEndpoint extends DefaultEndpoint {
public void setLdapConnectionName(String ldapConnectionName) {
this.ldapConnectionName = ldapConnectionName;
}
+
+ public boolean isAllowUrlBody() {
+ return allowUrlBody;
+ }
+
+ /**
+ * Whether to allow a message body that is not LDIF content to be
dereferenced as a URL and fetched. When disabled
+ * (default), a body that does not start with <tt>version: 1</tt> is
rejected with an
+ * {@link IllegalArgumentException} instead of being fetched as a URL,
which avoids a content-sniffed URL fetch
+ * (SSRF) from untrusted body content.
+ */
+ public void setAllowUrlBody(boolean allowUrlBody) {
+ this.allowUrlBody = allowUrlBody;
+ }
}
diff --git
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
index 98c86f832c60..8b73240ea138 100644
---
a/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
+++
b/components/camel-ldif/src/main/java/org/apache/camel/component/ldif/LdifProducer.java
@@ -59,8 +59,11 @@ public class LdifProducer extends DefaultProducer {
* Process the body. There are two options:
* <ol>
* <li>A String body that is the LDIF content. This needs to start with
"version: 1".</li>
- * <li>A String body that is a URL to ready the LDIF content from</li>
+ * <li>A String body that is a URL to read the LDIF content from - only
when the allowUrlBody option is
+ * enabled.</li>
* </ol>
+ * When the body is not LDIF content and allowUrlBody is disabled (the
default), an {@link IllegalArgumentException}
+ * is thrown instead of dereferencing the body as a URL.
*/
@Override
public void process(Exchange exchange) throws Exception {
@@ -76,7 +79,7 @@ public class LdifProducer extends DefaultProducer {
} else if (body.startsWith(LDIF_HEADER)) {
LOG.debug("Reading from LDIF body");
result = processLdif(new StringReader(body));
- } else {
+ } else if (((LdifEndpoint) getEndpoint()).isAllowUrlBody()) {
URL loc;
try {
loc = URI.create(body).toURL();
@@ -88,6 +91,10 @@ public class LdifProducer extends DefaultProducer {
}
throw new InvalidPayloadException(exchange, String.class);
}
+ } else {
+ throw new IllegalArgumentException(
+ "LDIF body does not start with '" + LDIF_HEADER
+ + "'. To dereference a non-LDIF
body as a URL, enable the allowUrlBody option on the ldif endpoint.");
}
exchange.getMessage().setBody(result);
diff --git
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifAllowUrlBodyTest.java
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifAllowUrlBodyTest.java
new file mode 100644
index 000000000000..22be9280414e
--- /dev/null
+++
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifAllowUrlBodyTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ldif;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A non-LDIF body (one that does not start with {@code version: 1}) is
dereferenced as a URL only when
+ * {@code allowUrlBody} is enabled. By default it is rejected instead of being
fetched, which avoids a content-sniffed
+ * URL fetch (SSRF) from untrusted body content. The rejection happens before
any LDAP connection is used, so this test
+ * needs no LDAP server. See CAMEL-24297.
+ */
+class LdifAllowUrlBodyTest extends CamelTestSupport {
+
+ @Test
+ void nonLdifBodyIsRejectedByDefault() {
+ CamelExecutionException ex =
assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:ldif",
"http://example.com/evil.ldif"));
+ assertInstanceOf(IllegalArgumentException.class, ex.getCause());
+ assertTrue(ex.getCause().getMessage().contains("allowUrlBody"));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:ldif").to("ldif:myConnection");
+ }
+ };
+ }
+}
diff --git
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
index 5c547423dcfd..81159d936b64 100644
---
a/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
+++
b/components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifRouteIT.java
@@ -61,7 +61,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
public class LdifRouteIT extends LdifTestSupport {
// Constants
private static final String LDAP_CONN_NAME = "conn";
- private static final String ENDPOINT_LDIF = "ldif:" + LDAP_CONN_NAME;
+ // these tests feed a URL as the body, so URL dereferencing must be
explicitly enabled (CAMEL-24297)
+ private static final String ENDPOINT_LDIF = "ldif:" + LDAP_CONN_NAME +
"?allowUrlBody=true";
private static final String ENDPOINT_START = "direct:start";
private static final String ENDPOINT_SETUP_START = "direct:setup";
private static final SearchControls SEARCH_CONTROLS
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
index 730ca59ab04c..1be9151d837a 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java
@@ -60,6 +60,7 @@ public final class SecurityUtils {
map.put("allowjavaserializedobject", new
SecurityOption(INSECURE_SERIALIZATION, "true"));
map.put("allowlocalwebhookurls", new SecurityOption(INSECURE_DEV,
"true"));
map.put("allowserializedheaders", new
SecurityOption(INSECURE_SERIALIZATION, "true"));
+ map.put("allowurlbody", new SecurityOption(INSECURE_DEV, "true"));
map.put("devconsoleenabled", new SecurityOption(INSECURE_DEV, "true"));
map.put("downloadenabled", new SecurityOption(INSECURE_DEV, "true"));
map.put("failonunknownhost", new SecurityOption(INSECURE_SSL,
VALUE_FALSE));
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 0ab406ff49dd..53f4ccba879e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -1537,6 +1537,15 @@ data format and the iterator/splitter modes. The full,
unmodified entry name rem
so routes that intentionally recreate the archive's directory structure keep
working — read it
from `CamelTarFileEntryName` for tar and from `zipFileName` for zip instead of
`CamelFileName`.
+=== camel-ldif - a non-LDIF body is no longer dereferenced as a URL by default
+
+The ldif producer previously treated a message body that does not start with
`version: 1` as a URL
+and dereferenced it (`URI.create(body).toURL().openStream()`). This
content-sniffed URL fetch is now
+opt-in: a new `allowUrlBody` option (default `false`) gates it. With the
default, a body that is not
+LDIF content is rejected with an `IllegalArgumentException` instead of being
fetched, which avoids a
+content-sniffed URL fetch (SSRF) from untrusted body content. Routes that rely
on passing a URL as the
+body must set `allowUrlBody=true` on the `ldif` endpoint.
+
=== camel-snakeyaml - typeFilters are now also enforced by the SnakeYAML
TagInspector
When `typeFilters` (or `unmarshalType`) is configured, the allow-list is now
also enforced by the
diff --git
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
index 27a52ad1ee1a..293173ed2898 100644
---
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
+++
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LdifEndpointBuilderFactory.java
@@ -44,6 +44,46 @@ public interface LdifEndpointBuilderFactory {
return (AdvancedLdifEndpointBuilder) this;
}
+ /**
+ * Whether to allow a message body that is not LDIF content to be
+ * dereferenced as a URL and fetched. When disabled (default), a body
+ * that does not start with version: 1 is rejected with an
+ * IllegalArgumentException instead of being fetched as a URL, which
+ * avoids a content-sniffed URL fetch (SSRF) from untrusted body
+ * content.
+ *
+ * The option is a: <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: security
+ *
+ * @param allowUrlBody the value to set
+ * @return the dsl builder
+ */
+ default LdifEndpointBuilder allowUrlBody(boolean allowUrlBody) {
+ doSetProperty("allowUrlBody", allowUrlBody);
+ return this;
+ }
+ /**
+ * Whether to allow a message body that is not LDIF content to be
+ * dereferenced as a URL and fetched. When disabled (default), a body
+ * that does not start with version: 1 is rejected with an
+ * IllegalArgumentException instead of being fetched as a URL, which
+ * avoids a content-sniffed URL fetch (SSRF) from untrusted body
+ * content.
+ *
+ * The option will be converted to a <code>boolean</code> type.
+ *
+ * Default: false
+ * Group: security
+ *
+ * @param allowUrlBody the value to set
+ * @return the dsl builder
+ */
+ default LdifEndpointBuilder allowUrlBody(String allowUrlBody) {
+ doSetProperty("allowUrlBody", allowUrlBody);
+ return this;
+ }
}
/**