diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/RouterSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/RouterSpec.java index 6d7e269c8b..14f19a532f 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/RouterSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/RouterSpec.java @@ -109,6 +109,21 @@ public final class RouterSpec return _this(); } + /** + * By default, if a resolved channel key does not exist in the channel map, the key + * itself is used as the channel name, which we will attempt to resolve to a channel. + * Invoke this method to disable this feature. This could be useful to prevent + * malicious actors from generating a message that could cause the message to be + * routed to an unexpected channel, such as one upstream of the router, which would + * cause a stack overflow. + * @return the router spec. + * @since 5.2 + */ + public RouterSpec noChannelKeyFallback() { + this.handler.setChannelKeyFallback(false); + return _this(); + } + /** * @param key the key. * @param channelName the channelName. diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java b/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java index 10f8f91950..a91d13c83c 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java @@ -74,6 +74,8 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter private boolean resolutionRequired = true; + private boolean channelKeyFallback = true; + private volatile Map channelMappings = new LinkedHashMap<>(); @@ -116,6 +118,20 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter this.resolutionRequired = resolutionRequired; } + /** + * When true (default), if a resolved channel key does not exist in the channel map, + * the key itself is used as the channel name, which we will attempt to resolve to a + * channel. Set to false to disable this feature. This could be useful to prevent + * malicious actors from generating a message that could cause the message to be + * routed to an unexpected channel, such as one upstream of the router, which would + * cause a stack overflow. + * @param channelKeyFallback false to disable the fall back. + * @since 5.2 + */ + public void setChannelKeyFallback(boolean channelKeyFallback) { + this.channelKeyFallback = channelKeyFallback; + } + /** * Set a limit for how many dynamic channels are retained (for reporting purposes). * When the limit is exceeded, the oldest channel is discarded. @@ -241,23 +257,25 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter // if the channelMappings contains a mapping, we'll use the mapped value // otherwise, the String-based channelKey itself will be used as the channel name - String channelName = channelKey; + String channelName = this.channelKeyFallback ? channelKey : null; boolean mapped = false; if (this.channelMappings.containsKey(channelKey)) { channelName = this.channelMappings.get(channelKey); mapped = true; } - if (this.prefix != null) { - channelName = this.prefix + channelName; - } - if (this.suffix != null) { - channelName = channelName + this.suffix; - } - MessageChannel channel = resolveChannelForName(channelName, message); - if (channel != null) { - channels.add(channel); - if (!mapped && this.dynamicChannels.get(channelName) == null) { - this.dynamicChannels.put(channelName, channel); + if (channelName != null) { + if (this.prefix != null) { + channelName = this.prefix + channelName; + } + if (this.suffix != null) { + channelName = channelName + this.suffix; + } + MessageChannel channel = resolveChannelForName(channelName, message); + if (channel != null) { + channels.add(channel); + if (!mapped && this.dynamicChannels.get(channelName) == null) { + this.dynamicChannels.put(channelName, channel); + } } } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java b/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java new file mode 100644 index 0000000000..0d216213b2 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.integration.dsl.routers; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * @author Gary Russell + * @since 5.2 + * + */ +@SpringJUnitConfig +public class NoFallbackAllowedTests { + + @Test + void noStackOverflow(@Autowired Config config) { + config.flow() + .getInputChannel() + .send(new GenericMessage<>("foo", Collections.singletonMap("whereTo", "flow.input"))); + assertThat(config.queue().receive(0)).isNotNull(); + } + + @Configuration + @EnableIntegration + public static class Config { + + @Bean + public IntegrationFlow flow() { + return f -> f.route("headers.whereTo", r -> r + .noChannelKeyFallback() + .defaultOutputChannel(queue())); + } + + @Bean + public QueueChannel queue() { + return new QueueChannel(); + } + + } + +} diff --git a/src/reference/asciidoc/router.adoc b/src/reference/asciidoc/router.adoc index cb6da26d09..3fe26c191b 100644 --- a/src/reference/asciidoc/router.adoc +++ b/src/reference/asciidoc/router.adoc @@ -1122,6 +1122,11 @@ That map's setter method is exposed as a public method along with the 'setChanne These let you change, add, and remove router mappings at runtime, as long as you have a reference to the router itself. It also means that you could expose these same configuration options through JMX (see <<./jmx.adoc#jmx,JMX Support>>) or the Spring Integration control bus (see <<./control-bus.adoc#control-bus,Control Bus>>) functionality.  +IMPORTANT: Falling back to the channel key as the channel name is flexible and convenient. +However, if you don't trust the message creator, a malicious actor (who has knowledge of the system) could create a message that is routed to an unexpected channel. +For example, if the key is set to the channel name of the router's input channel, such a message would be routed back to the router, eventually resulting in a stack overflow error. +You may therefore wish to disable this feature (set the `channelKeyFallback` property to `false`), and change the mappings instead if needed. + [[dynamic-routers-control-bus]] ===== Manage Router Mappings using the Control Bus @@ -1256,7 +1261,7 @@ The `routingSlipIndex` remains unchanged. * When the `routingSlipIndex` exceeds the size of the routing slip `path` list, the algorithm moves to the default behavior for the standard `replyChannel` header. [[process-manager]] -===== Process Manager Enterprise Integration Pattern +==== Process Manager Enterprise Integration Pattern Enterprise integration patterns include the https://www.enterpriseintegrationpatterns.com/ProcessManager.html[process manager] pattern. You can now easily implement this pattern by using custom process manager logic encapsulated in a `RoutingSlipRouteStrategy` within the routing slip. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index bf00a7c36b..7fc966ca6d 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -130,3 +130,9 @@ See <<./mongodb.adoc#mongodb, MongoDB Support>> for more information. Simple Apache Avro transformers are now provided. See <<./transformers.adoc#avro-transformers, Avro Transformers>> for more information. + +[[x5.2-routers]] +==== Router Changes + +You can now disable falling back to the channel key as the channel bean name. +See <<./router.adoc#dynamic-routers, Dynamic Routers>> for more information.