From fe311e52eba18414b23cbec698bf0bd9502bc90f Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 3 Aug 2026 13:32:08 +0200 Subject: [PATCH] CAMEL-24297: make ldif URL-body dereferencing an explicit opt-in 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 --- .../apache/camel/catalog/components/ldif.json | 3 +- .../ldif/LdifEndpointConfigurer.java | 6 +++ .../ldif/LdifEndpointUriFactory.java | 3 +- .../org/apache/camel/component/ldif/ldif.json | 3 +- .../camel/component/ldif/LdifEndpoint.java | 17 ++++++ .../camel/component/ldif/LdifProducer.java | 11 +++- .../component/ldif/LdifAllowUrlBodyTest.java | 53 +++++++++++++++++++ .../camel/component/ldif/LdifRouteIT.java | 3 +- .../org/apache/camel/util/SecurityUtils.java | 1 + .../pages/camel-4x-upgrade-guide-4_22.adoc | 9 ++++ .../dsl/LdifEndpointBuilderFactory.java | 40 ++++++++++++++ 11 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 components/camel-ldif/src/test/java/org/apache/camel/component/ldif/LdifAllowUrlBodyTest.java 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 73c2de0d93309..009d5314aefed 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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." } + "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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "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 rejected with an IllegalArgumentException instead of being fetched as a URL, which avoids a content-sniffed URL fetch (SSRF) from untrusted body content." } } } 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 b7a30cf4d9ca4..d4cecef713580 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 boolean configure(CamelContext camelContext, Object obj, String name, Obj @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 getOptionType(String name, boolean ignoreCase) { 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 679fba82585b1..4fad9d7005965 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 ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(2); + Set 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 73c2de0d93309..009d5314aefed 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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." } + "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 producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "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 rejected with an IllegalArgumentException instead of being fetched as a URL, which avoids a content-sniffed URL fetch (SSRF) from untrusted body content." } } } 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 27f3f12fc1944..fda73cbb5fa0d 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.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 String getLdapConnectionName() { 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 version: 1 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 98c86f832c60a..8b73240ea1387 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 LdifProducer(LdifEndpoint endpoint, String ldapConnectionName) { * Process the body. There are two options: *
    *
  1. A String body that is the LDIF content. This needs to start with "version: 1".
  2. - *
  3. A String body that is a URL to ready the LDIF content from
  4. + *
  5. A String body that is a URL to read the LDIF content from - only when the allowUrlBody option is + * enabled.
  6. *
+ * 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 void process(Exchange exchange) throws Exception { } 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 void process(Exchange exchange) throws Exception { } 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 0000000000000..22be9280414ef --- /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 5c547423dcfda..81159d936b643 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 @@ 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 730ca59ab04c4..1be9151d837a1 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 record SecurityOption(String category, String insecureValue) { 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 aa4417a1e2a62..5f8390dd8bc17 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 @@ -1421,3 +1421,12 @@ entry name could escape the intended directory (Tar Slip / Zip Slip). data format and the iterator/splitter modes. The full, unmodified entry name remains available 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. 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 27a52ad1ee1aa..293173ed28985 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 @@ default AdvancedLdifEndpointBuilder advanced() { 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: boolean 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 boolean 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; + } } /**