CVE-2026-40989
Description
Spring Cloud Function versions prior to 3.2.16, 4.1.10, 4.2.6, 4.3.3, and 5.0.2 are vulnerable to infinite recursion leading to an Out-of-Memory error.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Spring Cloud Function versions prior to 3.2.16, 4.1.10, 4.2.6, 4.3.3, and 5.0.2 are vulnerable to infinite recursion leading to an Out-of-Memory error.
Vulnerability
An infinite recursion vulnerability exists in the routing layer of Spring Cloud Function, specifically in versions prior to 3.2.16, 4.1.10, 4.2.6, 4.3.3, and 5.0.2. This issue can be triggered under certain conditions related to function composition, bypassing a self-routing guard [1].
Exploitation
An attacker can exploit this vulnerability by crafting a specific request that triggers the infinite recursion within the routing layer. This typically involves manipulating function composition in a way that bypasses security checks designed to prevent such loops [1]. No specific authentication or user interaction requirements are detailed in the available references.
Impact
Successful exploitation of this vulnerability can lead to an Out-of-Memory (OOM) error. This can cause the application to become unresponsive or crash, resulting in a denial-of-service condition for legitimate users [1].
Mitigation
Patched versions of Spring Cloud Function are available: 3.2.16, 4.1.10, 4.2.6, 4.3.3, and 5.0.2. Users are advised to upgrade to these versions or later. Older, unsupported versions are also affected and should be upgraded if possible [1].
AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <3.2.16, <4.1.10, <4.2.6, <4.3.3, <5.0.2
Patches
26bcd55803c5aFix unbounded cache in FunctioinRegistry and recursive composition
2 files changed · +61 −1
spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java+34 −1 modified@@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -102,7 +103,7 @@ public class SimpleFunctionRegistry implements FunctionRegistry { private final Set<FunctionRegistration<?>> functionRegistrations = new CopyOnWriteArraySet<>(); - private final Map<String, FunctionInvocationWrapper> wrappedFunctionDefinitions = new HashMap<>(); + private final Map<String, FunctionInvocationWrapper> wrappedFunctionDefinitions; private final ConversionService conversionService; @@ -114,6 +115,8 @@ public class SimpleFunctionRegistry implements FunctionRegistry { private final FunctionProperties functionProperties; + private int wrappedFunctionDefinitionsCacheSize = 1000; + @Autowired(required = false) private FunctionAroundWrapper functionAroundWrapper; @@ -127,8 +130,21 @@ public SimpleFunctionRegistry(ConversionService conversionService, CompositeMess this.messageConverter = messageConverter; this.functionInvocationHelper = functionInvocationHelper; this.functionProperties = functionProperties; + this.wrappedFunctionDefinitions = new LinkedHashMap<String, FunctionInvocationWrapper>() { + @Override + protected boolean removeEldestEntry(Map.Entry<String, FunctionInvocationWrapper> eldest) { + boolean remove = size() > wrappedFunctionDefinitionsCacheSize; + if (remove) { + if (logger.isDebugEnabled()) { + logger.debug("Removing message channel from cache " + eldest.getKey()); + } + } + return remove; + } + }; } + /** * Will add provided {@link MessageConverter}s to the head of the stack of the existing MessageConverters. * @@ -471,6 +487,19 @@ public class FunctionInvocationWrapper implements Function<Object, Object>, Cons } } + public int hashCode() { + return this.functionDefinition.hashCode(); + } + + public boolean equals(Object obj) { + if (obj instanceof FunctionInvocationWrapper functionWrapper) { + if (functionWrapper.getFunctionDefinition().equals(this.getFunctionDefinition())) { + return true; + } + } + return false; + } + @SuppressWarnings("unchecked") public void postProcess() { if (this.postProcessor != null) { @@ -666,6 +695,10 @@ public boolean isRoutingFunction() { public <V> Function<Object, V> andThen(Function<? super Object, ? extends V> after) { Assert.isTrue(after instanceof FunctionInvocationWrapper, "Composed function must be an instanceof FunctionInvocationWrapper."); + if (this.equals(after)) { + throw new IllegalArgumentException("Attempt is made to compose '" + this + + "' function with itself '" + after + "' which is not allowed as it causes recursive condition."); + } if (FunctionTypeUtils.isMultipleArgumentType(this.inputType) || FunctionTypeUtils.isMultipleArgumentType(this.outputType) || FunctionTypeUtils.isMultipleArgumentType(((FunctionInvocationWrapper) after).inputType)
spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java+27 −0 modified@@ -61,6 +61,7 @@ import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.context.config.RoutingFunction; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -80,6 +81,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; /** * @@ -108,6 +110,31 @@ public void before() { System.clearProperty("spring.cloud.function.definition"); } + @Test + public void testBoundedFunctionCache() throws Exception { + FunctionCatalog catalog = this.configureCatalog(CompositionWithNullReturnInBetween.class); + Field wrappedFunctionDefinitionsCacheSizeField = ReflectionUtils + .findField(catalog.getClass(), "wrappedFunctionDefinitionsCacheSize"); + wrappedFunctionDefinitionsCacheSizeField.setAccessible(true); + wrappedFunctionDefinitionsCacheSizeField.set(catalog, 10); + catalog.lookup("echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2"); + catalog.lookup("echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1"); + assertThat(catalog.size()).isEqualTo(11); + } + + @Test + public void testCompositionWithItself() throws Exception { + FunctionCatalog catalog = this.configureCatalog(CompositionWithNullReturnInBetween.class); + try { + catalog.lookup(RoutingFunction.FUNCTION_NAME + "|" + RoutingFunction.FUNCTION_NAME); + failBecauseExceptionWasNotThrown(IllegalArgumentException.class); + } + catch (IllegalArgumentException e) { + // TODO: nothing + } + + } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void testEmptyPojoConversion() {
57b4a1c05a48Fix unbounded cache in FunctioinRegistry and recursive composition
2 files changed · +61 −1
spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java+34 −1 modified@@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -102,7 +103,7 @@ public class SimpleFunctionRegistry implements FunctionRegistry { private final Set<FunctionRegistration<?>> functionRegistrations = new CopyOnWriteArraySet<>(); - private final Map<String, FunctionInvocationWrapper> wrappedFunctionDefinitions = new HashMap<>(); + private final Map<String, FunctionInvocationWrapper> wrappedFunctionDefinitions; private final ConversionService conversionService; @@ -114,6 +115,8 @@ public class SimpleFunctionRegistry implements FunctionRegistry { private final FunctionProperties functionProperties; + private int wrappedFunctionDefinitionsCacheSize = 1000; + @Autowired(required = false) private FunctionAroundWrapper functionAroundWrapper; @@ -127,8 +130,21 @@ public SimpleFunctionRegistry(ConversionService conversionService, CompositeMess this.messageConverter = messageConverter; this.functionInvocationHelper = functionInvocationHelper; this.functionProperties = functionProperties; + this.wrappedFunctionDefinitions = new LinkedHashMap<String, FunctionInvocationWrapper>() { + @Override + protected boolean removeEldestEntry(Map.Entry<String, FunctionInvocationWrapper> eldest) { + boolean remove = size() > wrappedFunctionDefinitionsCacheSize; + if (remove) { + if (logger.isDebugEnabled()) { + logger.debug("Removing message channel from cache " + eldest.getKey()); + } + } + return remove; + } + }; } + /** * Will add provided {@link MessageConverter}s to the head of the stack of the existing MessageConverters. * @@ -484,6 +500,19 @@ public class FunctionInvocationWrapper implements Function<Object, Object>, Cons } } + public int hashCode() { + return this.functionDefinition.hashCode(); + } + + public boolean equals(Object obj) { + if (obj instanceof FunctionInvocationWrapper functionWrapper) { + if (functionWrapper.getFunctionDefinition().equals(this.getFunctionDefinition())) { + return true; + } + } + return false; + } + @SuppressWarnings("unchecked") public void postProcess() { if (this.postProcessor != null) { @@ -687,6 +716,10 @@ public boolean isRoutingFunction() { public <V> Function<Object, V> andThen(Function<? super Object, ? extends V> after) { Assert.isTrue(after instanceof FunctionInvocationWrapper, "Composed function must be an instanceof FunctionInvocationWrapper."); + if (this.equals(after)) { + throw new IllegalArgumentException("Attempt is made to compose '" + this + + "' function with itself '" + after + "' which is not allowed as it causes recursive condition."); + } if (FunctionTypeUtils.isMultipleArgumentType(this.inputType) || FunctionTypeUtils.isMultipleArgumentType(this.outputType) || FunctionTypeUtils.isMultipleArgumentType(((FunctionInvocationWrapper) after).inputType)
spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java+27 −0 modified@@ -61,6 +61,7 @@ import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.context.config.RoutingFunction; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -80,6 +81,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; /** * @@ -108,6 +110,31 @@ public void before() { System.clearProperty("spring.cloud.function.definition"); } + @Test + public void testBoundedFunctionCache() throws Exception { + FunctionCatalog catalog = this.configureCatalog(CompositionWithNullReturnInBetween.class); + Field wrappedFunctionDefinitionsCacheSizeField = ReflectionUtils + .findField(catalog.getClass(), "wrappedFunctionDefinitionsCacheSize"); + wrappedFunctionDefinitionsCacheSizeField.setAccessible(true); + wrappedFunctionDefinitionsCacheSizeField.set(catalog, 10); + catalog.lookup("echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2"); + catalog.lookup("echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1|echo2|echo1"); + assertThat(catalog.size()).isEqualTo(11); + } + + @Test + public void testCompositionWithItself() throws Exception { + FunctionCatalog catalog = this.configureCatalog(CompositionWithNullReturnInBetween.class); + try { + catalog.lookup(RoutingFunction.FUNCTION_NAME + "|" + RoutingFunction.FUNCTION_NAME); + failBecauseExceptionWasNotThrown(IllegalArgumentException.class); + } + catch (IllegalArgumentException e) { + // TODO: nothing + } + + } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void testCompositionWithNonExistingFunction() throws Exception {
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
1News mentions
0No linked articles in our index yet.