CVE-2026-40989
This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.
Overview
Spring Cloud Function is a project within the Spring Cloud ecosystem that promotes the implementation of business logic via plain Java functions. It provides a uniform programming model across serverless providers and the ability to run standalone via Spring Boot, and it ships a routing function (functionRouter) that dispatches incoming requests to a target function selected at runtime via the spring.cloud.function.definition property or a spring.cloud.function.definition request header. The function-composition mechanism lets callers chain multiple functions with a pipe delimiter (for example upper|reverse) directly in the function-definition string.
A medium-severity vulnerability (CVE-2026-40989) has been identified in SimpleFunctionRegistry in the spring-cloud-function-context module. The pre-fix FunctionInvocationWrapper.andThen(...) method has no guard against composing a function with itself; in particular, the routing function can be composed with itself (functionRouter|functionRouter), which causes the resulting wrapper, when looked up or invoked, to recursively re-enter routing and re-compose. Each iteration allocates a fresh FunctionInvocationWrapper and inserts it into the registry's wrappedFunctionDefinitions map, which is an unbounded HashMap with no eviction. A caller able to reach the function-routing layer (over HTTP via the Spring Cloud Function Web adapter, or any other transport that surfaces user-controlled function-definition strings) can therefore repeatedly drive the JVM into an OutOfMemoryError and take the application down.
Per OWASP: "The Denial of Service (DoS) attack is focused on making a resource (site, application, server) unavailable for the purpose it was designed. There are many ways to make a service unavailable for legitimate users by manipulating network packets, programming, logical, or resources handling vulnerabilities, among others." In this CVE the abuse path is a logical flaw (missing self-equality guard) compounded by a resource-handling flaw (unbounded function-wrapper cache); together they let a single class of malformed composition expressions exhaust JVM heap.
The CVSS v3.1 base score for this vulnerability is 5.4 (Medium) with vector AV:P/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:H. The published vector lists the attack vector as Physical, but the abuse pattern itself is a function-definition string accepted by any caller that can drive a routing lookup, so deployments that surface user-controlled function definitions over HTTP should treat reachability as broader than Physical implies.
This issue affects Spring Cloud Function <3.2.16, >=4.0.0 <=4.2.5, >=4.3.0 <=4.3.2, and >=5.0.0 <=5.0.1.
Details
Module Info
- Product: Spring Cloud Function
- Affected packages: spring-cloud-function-context
- Affected versions: <3.2.16, >=4.0.0 <=4.2.5, >=4.3.0 <=4.3.2, >=5.0.0 <=5.0.1
- GitHub repository
- Published packages
- Package manager: Maven
- Fixed in:
- NES for Spring Cloud Function 3.1.x, 3.2.x, 4.1.x, 4.2.x
- OSS Spring Cloud Function 4.3.3, 5.0.2
Vulnerability Info
The vulnerability is in SimpleFunctionRegistry in spring-cloud-function-context, specifically in FunctionInvocationWrapper.andThen(...) and in the lifetime management of the wrappedFunctionDefinitions cache that SimpleFunctionRegistry keeps for previously-resolved composition strings.
In Spring Cloud Function, every function (including the built-in routing function functionRouter) is registered in the catalog as a FunctionInvocationWrapper. When a caller asks the catalog to look up a composition string such as a|b|c, the registry resolves each segment to a wrapper and chains them via Function.andThen(...). The pre-fix andThen has no check that after is the same wrapper as this:
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 (FunctionTypeUtils.isMultipleArgumentType(this.inputType)
|| FunctionTypeUtils.isMultipleArgumentType(this.outputType)
|| FunctionTypeUtils.isMultipleArgumentType(((FunctionInvocationWrapper) after).inputType)
|| FunctionTypeUtils.isMultipleArgumentType(((FunctionInvocationWrapper) after).outputType)) {
// multi-argument-type branch
}
// composition proceeds and the composed wrapper is registered
}FunctionInvocationWrapper did not override equals/hashCode, so two wrappers around the same underlying function definition were not equal even by reference-comparison standards. There was no other guard in the call chain, so a caller could legitimately ask the catalog to look up functionRouter|functionRouter. The composed wrapper, when invoked, dispatches into the routing function, which selects a target via the spring.cloud.function.definition setting; if that setting itself names a composition involving functionRouter, the routing function calls back into routing, which composes again, which re-enters routing, and so on. Each step allocates a new FunctionInvocationWrapper and inserts it into the registry's wrapper cache.
The cache itself is unbounded:
private final Map<String, FunctionInvocationWrapper> wrappedFunctionDefinitions = new HashMap<>();There is no cap and no eviction. Once a composition string has been resolved, its wrapper sits in the map for the lifetime of the application context. Under the recursive-composition pattern above, the map fills up with one entry per recursion step, JVM heap is exhausted, and the application dies with OutOfMemoryError. On a Spring Cloud Function Web deployment the routing function is reachable over HTTP, so this is a remotely-triggerable application-layer denial of service.
The fix combines two changes that together close the abuse path:
First, FunctionInvocationWrapper gains real value-equality, keyed off the function definition:
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;
}Second, andThen rejects self-composition outright:
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.");
}
// ...
}A composition request such as functionRouter|functionRouter now fails fast with IllegalArgumentException at lookup time, before any wrapper is materialized or cached.
Third, the wrapper cache is no longer unbounded. The constructor swaps the bare HashMap for a LinkedHashMap whose removeEldestEntry hook caps the cache at wrappedFunctionDefinitionsCacheSize (default 1000):
private int wrappedFunctionDefinitionsCacheSize = 1000;
// in the SimpleFunctionRegistry constructor
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;
}
};This is defense in depth: even if a future bypass smuggled in a new flavor of recursive or otherwise miss-heavy composition lookups, the cache will not grow beyond the configured cap.
Spring's own mitigation guidance ("ensure functions can not be composed with itself") corresponds directly to the this.equals(after) guard, and the upstream regression test composes the routing function with itself and asserts that the lookup throws IllegalArgumentException.
Steps To Reproduce
1. Deploy a Spring Cloud Function Web application on an affected pre-fix version (for example 4.3.2):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<version>4.3.2</version>
</dependency> @SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public Function<String, String> echo() {
return value -> value;
}
}2. Drive a function lookup that composes the routing function with itself. The simplest reproducer uses the Spring Cloud Function FunctionCatalog directly and matches the upstream regression test:
FunctionCatalog catalog = applicationContext.getBean(FunctionCatalog.class);
catalog.lookup("functionRouter|functionRouter"); // recurses, allocates, caches; eventually OOM3. Equivalently, on a Spring Cloud Function Web deployment that surfaces routing over HTTP, a caller able to set spring.cloud.function.definition (via property, environment, or a spring.cloud.function.definition header where the application accepts it) can request the same self-composition and trigger the same allocation pattern. Repeated requests of this shape exhaust JVM heap and produce OutOfMemoryError.
4. After upgrading to a fixed version (4.3.3 or 5.0.2, or a HeroDevs NES build for 3.1.x, 3.2.x, 4.1.x, or 4.2.x), the same lookup returns immediately with IllegalArgumentException: Attempt is made to compose '...' function with itself '...' which is not allowed as it causes recursive condition. and no wrappers are added to the cache.
Mitigation
Only recent versions of Spring Cloud Function receive community support and updates. Older versions have no publicly available fixes for this vulnerability.
Users of the affected components should apply one of the following mitigations:
- Upgrade to a currently supported version of Spring Cloud Function. The OSS fix ships in Spring Cloud Function 4.3.3 (4.3.x line) and 5.0.2 (5.0.x line).
- Leverage a commercial support partner like HeroDevs for post-EOL security support through Never-Ending Support (NES) for Spring Cloud Function. The HeroDevs NES builds for 3.1.x, 3.2.x, 4.1.x, and 4.2.x carry the same fix.
As an interim hardening step on a vulnerable deployment, ensure the function-definition strings reaching the routing layer cannot reference the routing function on both sides of a composition (for example by validating or constraining the values accepted for spring.cloud.function.definition and any header that overrides it), so that functionRouter|functionRouter and equivalent self-composition expressions never reach SimpleFunctionRegistry.lookup.